Strings
String คือลำดับของ Unicode characters
หัวข้อที่มีชื่อว่า “String คือลำดับของ Unicode characters”Python 3 strings (str) เป็นลำดับ Unicode code points ที่เปลี่ยนแปลงไม่ได้
Single quotes และ double quotes ใช้แทนกันได้ — เลือกแบบหนึ่งแล้วใช้สม่ำเสมอ
Triple quotes (""" หรือ ''') อนุญาตให้สร้าง string หลายบรรทัด
a = "Hello"b = 'World'c = """This spansmultiple lines."""String methods ที่ใช้บ่อย
หัวข้อที่มีชื่อว่า “String methods ที่ใช้บ่อย”String มี methods มากมาย เหล่านี้คือที่คุณจะใช้เป็นประจำ:
s = " Hello, Python! "print(s.strip()) # "Hello, Python!"print(s.upper()) # " HELLO, PYTHON! "print(s.lower()) # " hello, python! "print(s.replace("Python", "World")) # " Hello, World! "print(s.startswith(" H")) # Trueprint(s.endswith("! ")) # Trueprint(len(s)) # 18Methods คืน string ใหม่ — ไม่เคยแก้ไข string ต้นฉบับ
Slicing
หัวข้อที่มีชื่อว่า “Slicing”String รองรับไวยากรณ์ slicing แบบเดียวกับ list: s[start:stop:step]
Index ติดลบนับจากท้าย
s = "Hello, Python!"print(s[0:5]) # Helloprint(s[7:]) # Python!print(s[-7:]) # Python!print(s[::-1]) # !nohtyP ,olleH (reversed)print(s[::2]) # Hlo yhnbytes เทียบกับ str
หัวข้อที่มีชื่อว่า “bytes เทียบกับ str”str คือข้อความ — ลำดับ Unicode code point ที่เปลี่ยนแปลงไม่ได้ ไม่มีขนาด byte ในตัวเอง bytes คือข้อมูลดิบ — ลำดับจำนวนเต็ม 0–255 ที่เปลี่ยนแปลงไม่ได้ ทุกอย่างที่ผ่าน network socket, file ที่เปิดแบบ binary mode, หรือ API เข้ารหัส/hash ส่วนใหญ่ล้วนเป็น bytes ไม่ใช่ str .encode() แปลง str เป็น bytes โดยใช้ encoding ที่ระบุชื่อ .decode() แปลง bytes กลับเป็น str และ raise UnicodeDecodeError ถ้า byte นั้นไม่ถูกต้องสำหรับ encoding นั้น:
s = "café"b = s.encode("utf-8")print(b) # b'caf\xc3\xa9'print(len(s), len(b)) # 4 5 -- "é" is one character but two UTF-8 bytesprint(b.decode("utf-8")) # café
b.decode("ascii") # UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 ...len(s) นับตัวอักษร; len(b) นับbyte — มันจะเท่ากันก็ต่อเมื่อทุกตัวอักษรเป็น ASCII การ decode ด้วย encoding ผิดเป็นต้นเหตุของ crash ที่พบบ่อยเมื่อข้อมูลข้าม boundary (file, network response, subprocess) โดยไม่มี encoding= ที่ชัดเจนตรงกันทั้งสองฝั่ง
f-strings (formatted string literals)
หัวข้อที่มีชื่อว่า “f-strings (formatted string literals)”f-strings เปิดตัวใน Python 3.6 เป็นวิธีมาตรฐานในการฝังนิพจน์ใน string
ใส่ prefix f ก่อน string แล้ววางนิพจน์ Python ใดก็ได้ไว้ใน {}
name = "Alice"age = 30pi = 3.14159
print(f"Name: {name}, Age: {age}")print(f"Pi to 2dp: {pi:.2f}")print(f"2 + 2 = {2 + 2}")print(f"Upper: {name.upper()}")Format specifiers หลัง : ควบคุมการจัดวาง, ความแม่นยำ, และชนิด — mini-language เดียวกับที่ใช้ใน str.format()
t-strings (template strings, Python 3.14)
หัวข้อที่มีชื่อว่า “t-strings (template strings, Python 3.14)”f-string ถูก evaluate ทันทีและคืน str ที่เสร็จแล้ว ซึ่งเป็นปัญหาพอดีเวลาผลลัพธ์ถูกส่งต่อไปเป็น SQL, คำสั่ง shell
หรือ HTML เพราะตอนที่ code เห็น string นั้น ค่าที่แทรกเข้าไปแยกไม่ออกจากข้อความ literal รอบ ๆ แล้ว
t-string (PEP 750, Python 3.14) ใช้ prefix t และ evaluate เป็น Template แทนที่จะเป็น str
ส่วนที่เป็นข้อความ literal กับค่าที่แทรกเข้าไปจะยังแยกจากกัน library จึง escape เฉพาะค่าได้ก่อนประกอบเป็นข้อความสุดท้าย
variety = "Stilton"template = t"Try some {variety} cheese!"
print(template.strings) # ('Try some ', ' cheese!')print(template.interpolations[0].value) # Stiltonprint(template.interpolations[0].expression) # varietyการ iterate Template จะได้ส่วน literal เป็น str และส่วนที่แทนค่าเป็น object Interpolation —
นี่คือสิ่งที่ทำให้ render อย่างปลอดภัยได้:
from string.templatelib import Interpolation, Template
variety = "Stilton"template = t"Try some {variety} cheese!"
print(type(template).__name__)print(template.strings)
for part in template: if isinstance(part, Interpolation): print("value:", part.value, "| expression:", part.expression) else: print("literal:", repr(part))
def shout(tpl: Template) -> str: out = [] for part in tpl: out.append(str(part.value).upper() if isinstance(part, Interpolation) else part) return "".join(out)
print(shout(t"hello {variety}, and {1 + 1} more"))Loading Python runtime (first run only)…
ใช้ f-string เมื่อต้องการข้อความเดี๋ยวนี้ ใช้ t-string เมื่อมีอะไรปลายทางที่ต้องรู้ว่าส่วนไหนมาจากโปรแกรมของคุณ และส่วนไหนมาจากข้อมูล
เลือกใช้ str.format, f-string, หรือ t-string
หัวข้อที่มีชื่อว่า “เลือกใช้ str.format, f-string, หรือ t-string”ทั้งสามแบบใช้ mini-language {}/format-spec เดียวกัน แต่ตอบคำถามต่างกัน:
| รูปแบบ | evaluate เป็น | เมื่อไหร่ |
|---|---|---|
str.format(...) | str ทันที | template string เป็นข้อมูล — อ่านจาก config file, ตารางแปลภาษา, หรือมาจากผู้ใช้ตอนรันไทม์ จึงเป็น f-string literal ใน source ไม่ได้ |
| f-string | str ทันที | ต้องการข้อความที่เสร็จแล้วทันที ใน source code ของตัวเอง และไม่มีอะไรปลายทางต้องแยกค่าออกจากข้อความ literal |
| t-string | Template แบบ lazy | ผลลัพธ์ต้องไปที่ที่ต้อง escape หรือ validate ค่าแยกจากข้อความ literal — SQL, HTML, คำสั่ง shell, logging framework ที่ redact field |
ใช้ f-string เมื่อต้องการข้อความเดี๋ยวนี้ ใช้ t-string เมื่อมีอะไรปลายทางต้องรู้ว่าส่วนไหนมาจากโปรแกรมของคุณ
ใช้ str.format เฉพาะเมื่อ template เองไม่ใช่ literal ใน source
join() และ split()
หัวข้อที่มีชื่อว่า “join() และ split()”str.split(sep) แบ่ง string เป็น list
sep.join(iterable) รวม list ของ string เป็น string เดียว
ทั้งสองเป็นการกลับกันของกัน
csv = "one,two,three,four"parts = csv.split(",") # ['one', 'two', 'three', 'four']rejoined = " | ".join(parts) # 'one | two | three | four'print(parts)print(rejoined)การ concatenate string ใน loop เป็น quadratic
หัวข้อที่มีชื่อว่า “การ concatenate string ใน loop เป็น quadratic”str เปลี่ยนแปลงไม่ได้ ดังนั้น result += "x" ไม่ได้ขยาย result ในที่ — มันสร้าง string object ใหม่ทั้งหมด คัดลอกทุกตัวอักษรเดิมบวกตัวใหม่ แล้ว rebind result ไปที่มัน ทำแบบนี้ n ครั้งจะคัดลอกตัวอักษรรวมประมาณ 1 + 2 + ... + n ตัว: O(n²) ส่วน "".join(pieces) คำนวณความยาวรวมครั้งเดียวแล้วคัดลอกแต่ละตัวอักษรครั้งเดียว: O(n)
import time
n = 20000start = time.perf_counter()result = ""for i in range(n): result += "x"concat_time = time.perf_counter() - start
start = time.perf_counter()joined = "".join("x" for _ in range(n))join_time = time.perf_counter() - start
print(result == joined) # Trueprint(concat_time > join_time) # True (machine-dependent, but the trend holds)ใช้ string method join เสมอ (", ".join(items)) ไม่ควร concatenate ใน loop — การ concatenate ใน loop สร้าง string กลางที่ไม่จำเป็น O(n²) ชิ้น
กับดัก cutset ของ strip()
หัวข้อที่มีชื่อว่า “กับดัก cutset ของ strip()”s.strip(chars) ไม่ได้ลบ literal prefix/suffix substring — chars ถูกตีความเป็น set ของตัวอักษรแต่ละตัว ที่ถูกลบจากทั้งสองฝั่งตราบเท่าที่ยังตรงกันอยู่:
domain = "www.example.com"print(domain.strip("www.")) # "example.com" -- looks correct... but only by luck
messy = "www.wow.com"print(messy.strip("www.")) # "ow.com" -- also eats the "w" that starts "wow"print(messy.removeprefix("www.")) # "wow.com" -- compares a literal substring insteadstrip("www.") สร้าง cutset {'w', '.'} — ตัวอักษร w หรือ . ที่ต่อกันเป็นชุดตรงปลายด้านใดด้านหนึ่งจะถูกลบ ไม่ว่าจะเรียงลำดับยังไงหรือมีกี่ตัว มันบังเอิญให้คำตอบ “ถูกต้อง” สำหรับ domain เพราะตัวอักษรขอบของ example.com ไม่มีตัวไหนเป็น w หรือ . เลย ทันทีที่เนื้อหาจริงเริ่มด้วยตัวอักษรเหล่านั้น ("wow.com") strip จะกินเข้าไปในเนื้อหาด้วย ใช้ removeprefix() / removesuffix() (Python 3.9+) เมื่อต้องการ “ลบ substring นี้เป๊ะ ๆ” และเก็บ strip() ไว้สำหรับ “ตัดตัวอักษรเหล่านี้ตัวไหนก็ได้” (เช่น whitespace)
repr() เทียบกับ str()
หัวข้อที่มีชื่อว่า “repr() เทียบกับ str()”str(obj) สร้างรูปแบบที่มนุษย์อ่านง่าย repr(obj) สร้างรูปแบบที่ชัดเจนไม่กำกวมสำหรับ developer — ในอุดมคติคือรูปแบบที่วางกลับเข้า Python แล้วสร้าง object เดิมได้ print() ใช้ str(); REPL และ container (การ print list ของ string) ใช้ repr():
text = "line1\nline2"print(str(text)) # line1 # line2 (an actual newline)print(repr(text)) # 'line1\nline2' (the escape sequence, visible, on one line)นี่คือเหตุผลที่ print(["a\n", "b"]) แสดง ['a\n', 'b'] โดยเห็น escape ชัดเจน แม้ print("a\n") เดี่ยว ๆ จะแสดงการขึ้นบรรทัดใหม่จริง — str() ของ list ตกกลับไปเรียก repr() กับแต่ละ element แม่นยำเพื่อให้ escape character และขอบเขต quote ยังมองเห็นได้ แทนที่จะปนกันในผลลัพธ์
Demo ที่รันได้จริง
หัวข้อที่มีชื่อว่า “Demo ที่รันได้จริง”s = "Hello, Python!"print(s.upper())print(s.lower())print(s.replace("Python", "World"))print(s.startswith("Hello"))print(len(s))
print(s[0:5])print(s[-7:])print(s[::2])
name = "Alice"age = 30pi = 3.14159print(f"Name: {name}, Age: {age}")print(f"Pi to 2dp: {pi:.2f}")
words = ["one", "two", "three"]joined = ", ".join(words)print(joined)parts = "a:b:c:d".split(":")print(parts)
try: s[0] = "h"except TypeError as e: print(f"TypeError: {e}")Loading Python runtime (first run only)…
Playground ที่สองเป็นทัวร์ของกับดักด้านบน: encoding/decoding, กับดัก cutset ของ strip(), repr เทียบกับ str, และต้นทุน quadratic ของการ concatenate ใน loop
import time
# 1. bytes vs str: encoding, decoding, and the errors that cross the boundary.s = "café"b = s.encode("utf-8")print("encoded:", b)print("len(str) vs len(bytes):", len(s), len(b))print("decoded back:", b.decode("utf-8"))try: b.decode("ascii")except UnicodeDecodeError as e: print(f"UnicodeDecodeError: {e}")
# 2. The strip() cutset trap: the argument is a SET of characters, not a substring.domain = "www.example.com"print(domain.strip("www.")) # "example.com" -- looks correct, but only by luck
messy = "www.wow.com"print(messy.strip("www.")) # over-strips: eats into "wow" tooprint(messy.removeprefix("www.")) # removeprefix compares a literal substring instead
# 3. repr() vs str().text = "line1\nline2"print(str(text)) # the actual two linesprint(repr(text)) # one line, escape sequence visible
# 4. += in a loop is O(n^2); "".join is O(n).n = 20000start = time.perf_counter()result = ""for i in range(n): result += "x"concat_time = time.perf_counter() - start
start = time.perf_counter()joined = "".join("x" for _ in range(n))join_time = time.perf_counter() - start
print("same content:", result == joined)print("concatenation was slower than join:", concat_time > join_time)Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”str ไม่ใช่ buffer ของตัวอักษรที่แก้ไขได้ — ทุกการเรียก method, ทุกการ concatenate, และทุก slice สร้าง str object ใหม่ที่เป็นอิสระจากกัน สองชื่อชี้ไปที่ string เดียวกันได้อย่างปลอดภัย (ต่างจาก list) เพราะไม่มีอะไรแก้ไขมันได้จากใต้ชื่อไหนเลย bytes ก็ตามโมเดล immutable-value เดียวกัน แค่อยู่อีกชั้นหนึ่ง — ก่อนที่การ decode Unicode จะเกิดขึ้น
flowchart LR
str1["str: 'café'"] -->|".encode('utf-8')"| bytesObj["bytes: b'caf\xc3\xa9'"]
bytesObj -->|".decode('utf-8')"| str2["str: 'café' (new object)"]
str1 -->|".upper()"| str3["str: 'CAFÉ' (new object)"] ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
str ที่ immutable | แชร์ข้ามชื่อ/thread ได้อย่างปลอดภัย ใช้เป็น dict key ได้ | ทุกการแปลงจอง string ใหม่ — รวมถึงตัวที่สร้างทีละตัวอักษร |
การแยก bytes/str | ไม่มีความกำกวมว่าข้อมูลถูก decode เป็นข้อความแล้วหรือยังเป็น byte ดิบ | ทุก I/O boundary ต้องมี encoding= ชัดเจน เดาผิดแล้ว raise UnicodeDecodeError |
| f-string | เร็ว อ่านง่าย evaluate ตรงจุดที่เขียน | template เองเป็นข้อมูลภายนอก (ค่าใน config, การแปลภาษา) ไม่ได้ |
| t-string | ค่ายังตรวจสอบและ escape ได้ก่อนประกอบ | ต้องเรียนรู้เพิ่มอีกหนึ่งแนวคิด ไม่มีอะไรใช้ Template เปล่า ๆ ได้โดยไม่มี library รองรับ |
ความหมาย cutset ของ strip() | method เดียวจัดการ “ตัดตัวอักษรเหล่านี้ตัวไหนก็ได้” มีประโยชน์กับ whitespace | เข้าใจผิดง่ายว่าเป็นการลบ literal prefix/suffix ซึ่งเป็นคนละ operation กัน (removeprefix/removesuffix) |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
len(a_string)กับlen(a_string.encode())เท่ากันเสมอ” เท่ากันเฉพาะข้อความ ASCII ล้วนเท่านั้น — ตัวอักษร multi-byte ใด ๆ ทำให้ byte length มากกว่าจำนวนตัวอักษร - “
s.strip("www.")ลบ literal prefix"www."” มันลบตัวอักษรwและ.ที่ต่อกันจากทั้งสองฝั่ง — เป็น set ของตัวอักษร ไม่ใช่ substring ใช้removeprefix/removesuffixสำหรับการลบแบบ literal - “
str(x)กับrepr(x)พิมพ์เหมือนกันเสมอ” มันตรงกันสำหรับตัวเลขธรรมดา แต่ต่างกันสำหรับ string (quote และ escape ปรากฏในrepr) และสำหรับ object ที่กำหนดเอง ซึ่งนิยามต่างกันได้เลย - “สร้าง string ด้วย
+=ใน loop เร็วพอ ๆ กับ''.join()” ไม่จริง —strเปลี่ยนแปลงไม่ได้ ทุก+=คัดลอก string ทั้งหมดที่มีอยู่เข้า object ใหม่ ทำให้ loop ของการ concatenatenครั้งเป็น O(n²) แทนที่จะเป็น O(n) - “f-string กับ t-string ทำสิ่งเดียวกันแค่ prefix ต่างกัน” f-string คือ
strที่เสร็จสมบูรณ์ทันทีที่เขียน ส่วน t-string คือTemplateobject ที่ยังเก็บส่วน literal กับส่วนที่แทรกค่าแยกจากกันให้อะไรบางอย่างปลายทางประมวลผลต่อ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- โค้ดสร้าง string ด้วย
+=ใน loop โดยเฉพาะที่อาจวนหลายรอบ แทนที่จะรวบรวมชิ้นส่วนใน list แล้วเรียก"".join(...)ครั้งเดียวหรือเปล่า? .strip(...),.lstrip(...), หรือ.rstrip(...)รับ argument หลายตัวอักษรที่ดูเหมือนตั้งใจ “ลบ prefix/suffix นี้” แทนที่จะเป็น “ลบตัวอักษรเหล่านี้ตัวไหนก็ได้” หรือเปล่า? นั่นควรเป็นremoveprefix/removesuffix- โค้ดผสม
strกับbytesใน expression เดียวกัน (เช่น concatenate ค่าstrกับbytes) หรือเรียก.encode()/.decode()โดยไม่มีencoding=ชัดเจนแล้วสมมติว่ามันสำเร็จเสมอหรือเปล่า? - log message หรือ debug print ใช้
str()(หรือ f-string) กับค่าที่รูปแบบเห็น escape ชัดเจนไม่กำกวมของrepr()จะมีประโยชน์กว่าในการวินิจฉัย bug จริง ๆ หรือเปล่า? - template string ถูกสร้างด้วยมือด้วย
%หรือ.format()ทั้งที่ string ไม่ได้มาจากภายนอก source file — เป็น f-string ได้ไหม หรือต้องเป็น t-string เพราะผลลัพธ์ไปที่ที่มีความเสี่ยง injection?
🐛 หา bug ในโค้ดนี้
def build_report(rows): text = "" for row in rows: text += f"{row}\n" return textเฉลย
function นี้ดูถูกต้อง — มันสร้าง string ที่ถูกต้องจริง ๆ — แต่สำหรับ rows ขนาดใหญ่มันเป็น O(n²) เงียบ ๆ: str เปลี่ยนแปลงไม่ได้ ดังนั้นทุก text += ... คัดลอก string ทั้งหมด ที่สร้างมาแล้วเข้า object ใหม่เพียงเพื่อเพิ่มอีกหนึ่งบรรทัด กับ rows พันแถวนั่นคือการคัดลอกตัวอักษรราวครึ่งล้านตัวแทนที่จะเป็นพันตัว ทางแก้คือรวบรวมชิ้นส่วนใน list แล้ว join ครั้งเดียว: return "\n".join(str(row) for row in rows) + "\n" ซึ่งคัดลอกแต่ละตัวอักษรครั้งเดียวไม่ว่าจะมีกี่ rows ก็ตาม