ข้ามไปยังเนื้อหา

Strings

Python 3 strings (str) เป็นลำดับ Unicode code points ที่เปลี่ยนแปลงไม่ได้ Single quotes และ double quotes ใช้แทนกันได้ — เลือกแบบหนึ่งแล้วใช้สม่ำเสมอ Triple quotes (""" หรือ ''') อนุญาตให้สร้าง string หลายบรรทัด

a = "Hello"
b = 'World'
c = """This spans
multiple lines."""

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")) # True
print(s.endswith("! ")) # True
print(len(s)) # 18

Methods คืน string ใหม่ — ไม่เคยแก้ไข string ต้นฉบับ

String รองรับไวยากรณ์ slicing แบบเดียวกับ list: s[start:stop:step] Index ติดลบนับจากท้าย

s = "Hello, Python!"
print(s[0:5]) # Hello
print(s[7:]) # Python!
print(s[-7:]) # Python!
print(s[::-1]) # !nohtyP ,olleH (reversed)
print(s[::2]) # Hlo yhn

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 bytes
print(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 เปิดตัวใน Python 3.6 เป็นวิธีมาตรฐานในการฝังนิพจน์ใน string ใส่ prefix f ก่อน string แล้ววางนิพจน์ Python ใดก็ได้ไว้ใน {}

name = "Alice"
age = 30
pi = 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()

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) # Stilton
print(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"))

ใช้ f-string เมื่อต้องการข้อความเดี๋ยวนี้ ใช้ t-string เมื่อมีอะไรปลายทางที่ต้องรู้ว่าส่วนไหนมาจากโปรแกรมของคุณ และส่วนไหนมาจากข้อมูล

ทั้งสามแบบใช้ mini-language {}/format-spec เดียวกัน แต่ตอบคำถามต่างกัน:

รูปแบบevaluate เป็นเมื่อไหร่
str.format(...)str ทันทีtemplate string เป็นข้อมูล — อ่านจาก config file, ตารางแปลภาษา, หรือมาจากผู้ใช้ตอนรันไทม์ จึงเป็น f-string literal ใน source ไม่ได้
f-stringstr ทันทีต้องการข้อความที่เสร็จแล้วทันที ใน source code ของตัวเอง และไม่มีอะไรปลายทางต้องแยกค่าออกจากข้อความ literal
t-stringTemplate แบบ lazyผลลัพธ์ต้องไปที่ที่ต้อง escape หรือ validate ค่าแยกจากข้อความ literal — SQL, HTML, คำสั่ง shell, logging framework ที่ redact field

ใช้ f-string เมื่อต้องการข้อความเดี๋ยวนี้ ใช้ t-string เมื่อมีอะไรปลายทางต้องรู้ว่าส่วนไหนมาจากโปรแกรมของคุณ ใช้ str.format เฉพาะเมื่อ template เองไม่ใช่ literal ใน source

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)

str เปลี่ยนแปลงไม่ได้ ดังนั้น result += "x" ไม่ได้ขยาย result ในที่ — มันสร้าง string object ใหม่ทั้งหมด คัดลอกทุกตัวอักษรเดิมบวกตัวใหม่ แล้ว rebind result ไปที่มัน ทำแบบนี้ n ครั้งจะคัดลอกตัวอักษรรวมประมาณ 1 + 2 + ... + n ตัว: O(n²) ส่วน "".join(pieces) คำนวณความยาวรวมครั้งเดียวแล้วคัดลอกแต่ละตัวอักษรครั้งเดียว: O(n)

import time
n = 20000
start = 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) # True
print(concat_time > join_time) # True (machine-dependent, but the trend holds)

ใช้ string method join เสมอ (", ".join(items)) ไม่ควร concatenate ใน loop — การ concatenate ใน loop สร้าง string กลางที่ไม่จำเป็น O(n²) ชิ้น

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 instead

strip("www.") สร้าง cutset {'w', '.'} — ตัวอักษร w หรือ . ที่ต่อกันเป็นชุดตรงปลายด้านใดด้านหนึ่งจะถูกลบ ไม่ว่าจะเรียงลำดับยังไงหรือมีกี่ตัว มันบังเอิญให้คำตอบ “ถูกต้อง” สำหรับ domain เพราะตัวอักษรขอบของ example.com ไม่มีตัวไหนเป็น w หรือ . เลย ทันทีที่เนื้อหาจริงเริ่มด้วยตัวอักษรเหล่านั้น ("wow.com") strip จะกินเข้าไปในเนื้อหาด้วย ใช้ removeprefix() / removesuffix() (Python 3.9+) เมื่อต้องการ “ลบ substring นี้เป๊ะ ๆ” และเก็บ strip() ไว้สำหรับ “ตัดตัวอักษรเหล่านี้ตัวไหนก็ได้” (เช่น whitespace)

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 ยังมองเห็นได้ แทนที่จะปนกันในผลลัพธ์

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 = 30
pi = 3.14159
print(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}")

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" too
print(messy.removeprefix("www.")) # removeprefix compares a literal substring instead
# 3. repr() vs str().
text = "line1\nline2"
print(str(text)) # the actual two lines
print(repr(text)) # one line, escape sequence visible
# 4. += in a loop is O(n^2); "".join is O(n).
n = 20000
start = 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)

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)"]
ทุก operation ของ string คืน object ใหม่; str กับ bytes แปลงกันผ่าน encoding ที่ชัดเจน
สิ่งที่ได้ประโยชน์ต้นทุน
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 ของการ concatenate n ครั้งเป็น O(n²) แทนที่จะเป็น O(n)
  • “f-string กับ t-string ทำสิ่งเดียวกันแค่ prefix ต่างกัน” f-string คือ str ที่เสร็จสมบูรณ์ทันทีที่เขียน ส่วน t-string คือ Template object ที่ยังเก็บส่วน literal กับส่วนที่แทรกค่าแยกจากกันให้อะไรบางอย่างปลายทางประมวลผลต่อ
  • โค้ดสร้าง 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 ก็ตาม

โค้ดนี้พิมพ์อะไร?
s = "café"
b = s.encode("utf-8")
print(len(s), len(b))
โค้ดนี้พิมพ์อะไร?
messy = "www.wow.com"
print(messy.strip("www."))
วิธี idiomatic ในการลบ literal prefix "www." ออกจาก string เป๊ะ ๆ โดยไม่เสี่ยงพฤติกรรม cutset ของ strip() คืออะไร?
โค้ดนี้พิมพ์อะไร?
print(str("a\nb"))
print(repr("a\nb"))
ทำไมการสร้าง string ขนาดใหญ่ด้วย result += piece ใน loop ถึงถือว่าเป็น O(n²)?
t-string (Python 3.14) evaluate ออกมาเป็นอะไร?