ทัวร์ Stdlib
ทัวร์แนะนำโมดูล stdlib ที่สำคัญ
หัวข้อที่มีชื่อว่า “ทัวร์แนะนำโมดูล stdlib ที่สำคัญ”Python standard library มีขนาดใหญ่ แต่มีโมดูลไม่กี่ตัวที่ปรากฏในแทบทุกโค้ดเบสจริงจัง
บทนี้ให้บทนำที่รันได้จริงของห้าตัวคือ collections, itertools, datetime, json และ pathlib — บวก functools, decimal, enum, secrets และ subprocess
collections — container type เฉพาะทาง
หัวข้อที่มีชื่อว่า “collections — container type เฉพาะทาง”โมดูล collections ต่อยอด container ในตัวของ Python ด้วยเครื่องมือหลักสี่ตัวที่คุณจะใช้ทุกวัน
Counter นับ object ที่ hashable ได้และรองรับการคำนวณทางคณิตศาสตร์บนจำนวนนับ
from collections import Counterc = Counter("aabbbcc")print(c) # Counter({'b': 3, 'a': 2, 'c': 2})print(c.most_common(2)) # [('b', 3), ('a', 2)]defaultdict ไม่ raise KeyError เลย — key ที่ไม่มีจะถูกสร้างอัตโนมัติด้วย factory ที่คุณให้มา
from collections import defaultdictdd = defaultdict(list)dd["fruits"].append("apple") # no KeyError on a fresh keynamedtuple สร้าง record type ที่เบาและ immutable พร้อมชื่อ field — อ่านง่ายกว่า tuple ธรรมดาและไม่มี overhead เพิ่มเมื่อเทียบกับ tuple ปกติ
from collections import namedtuplePoint = namedtuple("Point", ["x", "y"])p = Point(3, 4)print(p.x, p.y) # 3 4itertools — ตัวประกอบแบบ lazy
หัวข้อที่มีชื่อว่า “itertools — ตัวประกอบแบบ lazy”itertools มี iterator-building block ที่ประกอบกันได้ดีและไม่สร้าง sequence เต็มขึ้นในหน่วยความจำจนกว่าคุณจะขอ
chain รวมหลาย iterable เข้าเป็น stream เดียว
islice slice iterator ใดก็ได้แบบ lazy
groupby จัดกลุ่ม element ที่มี key เท่ากันติดกัน — ข้อมูลต้องถูก sortด้วย key เดียวกันมาก่อน
accumulate ให้ผลรวมสะสม (หรือ binary operation ใดก็ได้ที่สะสมไปเรื่อย ๆ) pairwise ให้คู่ที่ติดกันซ้อนทับกัน compress กรอง iterable หนึ่งด้วย iterable บูลีนที่ขนานกัน
import itertoolslist(itertools.chain([1, 2], [3, 4])) # [1, 2, 3, 4]list(itertools.islice(range(100), 5)) # [0, 1, 2, 3, 4]list(itertools.accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]list(itertools.pairwise([1, 2, 3, 4])) # [(1, 2), (2, 3), (3, 4)]list(itertools.compress("ABCDEF", [1, 0, 1, 0, 1, 1])) # ['A', 'C', 'E', 'F']Playground 1 — collections และ itertools
หัวข้อที่มีชื่อว่า “Playground 1 — collections และ itertools”import collectionsimport itertools
# --- Counter ---words = "the quick brown fox jumps over the lazy dog the fox".split()c = collections.Counter(words)print("top 3:", c.most_common(3))
# --- defaultdict ---dd = collections.defaultdict(list)for k, v in [("a", 1), ("b", 2), ("a", 3)]: dd[k].append(v)print("grouped:", dict(dd))
# --- namedtuple ---Point = collections.namedtuple("Point", ["x", "y"])p = Point(3, 4)print("point:", p, "| x =", p.x)
# --- chain ---merged = list(itertools.chain([1, 2], [3, 4], [5]))print("chained:", merged)
# --- groupby (data must be sorted) ---data = [("a", 1), ("a", 2), ("b", 3), ("b", 4)]for key, group in itertools.groupby(data, key=lambda t: t[0]): print(key, list(group))
# --- accumulate / pairwise / compress ---print("running totals:", list(itertools.accumulate([1, 2, 3, 4])))print("pairwise:", list(itertools.pairwise([1, 2, 3, 4])))print("compressed:", list(itertools.compress("ABCDEF", [1, 0, 1, 0, 1, 1])))Loading Python runtime (first run only)…
datetime — aware กับ naive และทำไมความต่างนี้ถึงสำคัญ
หัวข้อที่มีชื่อว่า “datetime — aware กับ naive และทำไมความต่างนี้ถึงสำคัญ”datetime มีสองแบบ: naive (ไม่มี timezone ติดอยู่ — เป็นแค่ตัวเลข กำกวมว่าหมายถึงช่วงเวลาไหนจริง ๆ) หรือ aware (มี tzinfo — จุดเวลาที่ไม่กำกวม) การผสมทั้งสองแบบจะ raise TypeError โดยตั้งใจ:
import datetime as dt
naive = dt.datetime(2026, 9, 22, 10, 0, 0)aware = dt.datetime(2026, 9, 22, 10, 0, 0, tzinfo=dt.timezone.utc)print(naive.tzinfo, aware.tzinfo) # None UTC
naive - aware# TypeError: can't subtract offset-naive and offset-aware datetimesdatetime.now() ที่ไม่มี argument คืนเวลาท้องถิ่นแบบ naive — เป็นต้นตอของ bug บ่อยครั้งเมื่อค่านั้นถูกเก็บ เปรียบเทียบ หรือส่งข้าม timezone ให้ใส่ timezone เสมอ:
now_utc = dt.datetime.now(dt.timezone.utc) # aware, unambiguousdatetime.utcnow() ดูเหมือนจะแก้ปัญหาแต่ไม่ใช่ — มันคืน datetime แบบ naive ที่บังเอิญถือตัวเลขของ UTC และถูก deprecate แล้ว:
import warningswarnings.simplefilter("always")dt.datetime.utcnow()# DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled# for removal in a future version. Use timezone-aware objects to represent# datetimes in UTC: datetime.datetime.now(datetime.UTC).สำหรับ timezone จริง (ไม่ใช่แค่ offset UTC คงที่ — กฎ DST จริง ภูมิภาคจริง) ใช้ zoneinfo (อยู่ใน stdlib ตั้งแต่ 3.9):
from zoneinfo import ZoneInfo
bkk = dt.datetime(2026, 9, 22, 10, 0, tzinfo=ZoneInfo("Asia/Bangkok"))print(bkk, "-> utc:", bkk.astimezone(dt.timezone.utc))# 2026-09-22 10:00:00+07:00 -> utc: 2026-09-22 03:00:00+00:00datetime.fromisoformat parse ข้อความ ISO 8601 ที่ JSON API, log และฐานข้อมูลผลิตออกมาจริง ๆ รวม offset ด้วย:
dt.datetime.fromisoformat("2026-09-22T10:00:00+07:00")# datetime.datetime(2026, 9, 22, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=25200)))ตัวอย่างด้านล่างใช้วันที่คงที่เพื่อให้ output แน่นอนเสมอ — เป็นนิสัยที่ดีเวลาเขียนตัวอย่างหรือเทส
json — การ serialize, default= และความแม่นยำ
หัวข้อที่มีชื่อว่า “json — การ serialize, default= และความแม่นยำ”json.dumps แปลง Python object เป็น JSON string; json.loads parse กลับ
argument separators ช่วยตัด whitespace ออกเพื่อ output ที่กระชับ
import jsons = json.dumps({"key": "value"}, separators=(",", ":"))# '{"key":"value"}'json รู้จักแค่ชุด Python type ที่กำหนดตายตัวเท่านั้น — Decimal (type ที่ถูกต้องสำหรับเงิน ดูด้านล่าง) จะ raise TypeError เว้นแต่คุณจะให้ default= ซึ่งเป็นทางสำรองที่ถูกเรียกกับอะไรก็ตามที่มันไม่รู้จัก:
from dataclasses import dataclass, asdictfrom decimal import Decimal
@dataclassclass Order: id: int total: Decimal
o = Order(1, Decimal("19.99"))json.dumps(asdict(o))# TypeError: Object of type Decimal is not JSON serializable
def default(obj): if isinstance(obj, Decimal): return str(obj) raise TypeError(f"not serializable: {type(obj)}")
json.dumps(asdict(o), default=default)# '{"id": 1, "total": "19.99"}'ensure_ascii=True (ค่า default) escape อักขระที่ไม่ใช่ ASCII ทุกตัวเป็นลำดับ \uXXXX — ปลอดภัยทุกที่ แต่อ่านไม่ออกสำหรับข้อความที่ไม่ใช่ภาษาอังกฤษ ตั้งเป็น False เมื่อคุณรู้ว่าฝั่งที่รับรองรับ UTF-8:
json.dumps({"city": "กรุงเทพ"}) # '{"city": "\\u0e01\\u0e23..."}'json.dumps({"city": "กรุงเทพ"}, ensure_ascii=False) # '{"city": "กรุงเทพ"}'Playground 2 — datetime และ json
หัวข้อที่มีชื่อว่า “Playground 2 — datetime และ json”import datetimeimport json
# Fixed date for determinismdt = datetime.datetime(2024, 3, 14, 9, 26, 53)print("pi day:", dt.strftime("%Y-%m-%d %H:%M:%S"))delta = datetime.timedelta(days=10)print("plus 10 days:", (dt + delta).date())
# aware vs naiveaware = dt.replace(tzinfo=datetime.timezone.utc)print("naive tzinfo:", dt.tzinfo, "| aware tzinfo:", aware.tzinfo)try: dt - awareexcept TypeError as e: print("TypeError:", e)
# json round-tripobj = {"name": "Python", "versions": [3, 10, 11]}s = json.dumps(obj, separators=(",", ":"))print("serialized:", s)back = json.loads(s)print("name:", back["name"], "| latest:", back["versions"][-1])
# ensure_asciiprint(json.dumps({"note": "café"}))print(json.dumps({"note": "café"}, ensure_ascii=False))Loading Python runtime (first run only)…
functools — lru_cache, partial, reduce, singledispatch
หัวข้อที่มีชื่อว่า “functools — lru_cache, partial, reduce, singledispatch”from functools import lru_cache, partial, reduce, singledispatch
@lru_cache(maxsize=128)def fib(n: int) -> int: return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(30) # 832040 -- cached after the first callfib.cache_info() # CacheInfo(hits=28, misses=31, maxsize=128, currsize=31)lru_cache ต้องการให้ทุก argument hashable — decorator cache ที่ AI สร้างบนฟังก์ชันที่รับ list จะล้มเหลวตอนเรียก ไม่ใช่ตอน decorate:
@lru_cachedef total(items: list) -> int: return sum(items)
total([1, 2, 3])# TypeError: unhashable type: 'list'partial ตรึง argument บางตัวของ callable ไว้ ได้ callable ใหม่ที่มีพารามิเตอร์น้อยลง — มีประโยชน์เวลาปรับฟังก์ชันให้เข้ากับ API ที่เรียกด้วยรูปแบบตายตัว reduce พับ iterable ลงเหลือค่าเดียวด้วยฟังก์ชัน binary singledispatch เลือก implementation ตาม type ของ argument ตัวแรก — ทางเลือกที่เบากว่าการเช็ค isinstance เป็นชุด:
add3 = partial(lambda a, b, c: a + b + c, 1, 2)add3(3) # 6
reduce(lambda acc, x: acc + x, [1, 2, 3, 4], 0) # 10
@singledispatchdef describe(x): return f"generic: {x!r}"
@describe.registerdef _(x: int): return f"int: {x}"
@describe.registerdef _(x: str): return f"str: {x!r}"
describe(5) # 'int: 5'describe("hi") # "str: 'hi'"describe(3.14) # 'generic: 3.14'pathlib — object-oriented filesystem path
หัวข้อที่มีชื่อว่า “pathlib — object-oriented filesystem path”pathlib.Path แทน filesystem path ด้วย object ที่มีเมธอดและ property แทนวิธีสตริงแบบเก่าของ os.path
from pathlib import Path
p = Path("/usr/local/lib/python3.14/site-packages/requests/__init__.py")print(p.suffix) # .pyprint(p.stem) # __init__print(p.parts) # ('/', 'usr', 'local', 'lib', 'python3.14', 'site-packages', 'requests', '__init__.py')
# Building paths with /project = Path.home() / "projects" / "myapp"config = project / "config.toml"print(config)open() ที่ไม่มี encoding= จะรับ encoding ข้อความ default ของแพลตฟอร์ม — UTF-8 บน Linux/macOS ส่วนใหญ่ทุกวันนี้ แต่ในอดีตเป็น cp1252 บน Windows ใส่ encoding="utf-8" ตรง ๆ เสมอ เพื่อให้โค้ดเดียวกันทำงานเหมือนกันทุกที่ รวมถึงตอนเนื้อหามีข้อความที่ไม่ใช่ ASCII:
p = Path("notes.txt")p.write_text("สวัสดี\n", encoding="utf-8")with open(p, encoding="utf-8") as f: print(repr(f.read())) # 'สวัสดี\n'
pathlibจัดการ filesystem path จริง — รันในเครื่อง; browser sandbox ไม่มี filesystem จริง
subprocess — รันโปรแกรมอื่นอย่างปลอดภัย
หัวข้อที่มีชื่อว่า “subprocess — รันโปรแกรมอื่นอย่างปลอดภัย”subprocess.run ที่มี check=True จะ raise CalledProcessError เมื่อ exit code ไม่เป็นศูนย์แทนที่จะรันต่อเงียบ ๆ capture_output=True เก็บ stdout/stderr แทนที่จะพ่นลง terminal timeout= ป้องกัน child process ที่ค้างไม่ให้ทำโปรแกรมของคุณค้างตลอดไป
import subprocess
result = subprocess.run( ["echo", "hello"], check=True, capture_output=True, text=True, timeout=5)print(result.stdout.strip()) # 'hello'
subprocess.run(["false"], check=True, capture_output=True, text=True)# subprocess.CalledProcessError: Command '['false']' returned non-zero exit status 1.
subprocess.run(["sleep", "5"], timeout=0.2)# subprocess.TimeoutExpired: Command '['sleep', '5']' timed out after 0.2 secondsshell=True กับสตริงที่ไม่น่าเชื่อถือคือช่องโหว่ command injection — ทั้งสตริงถูกส่งให้ shell ของระบบ รวมถึง ;, && และ backtick:
user_filename = "notes.txt; echo INJECTED"subprocess.run(f"ls {user_filename}", shell=True)# real output includes a line "INJECTED" -- the shell ran a second commandทางแก้คือส่ง list (ไม่มี shell เข้ามาเกี่ยวข้อง ไม่มีการตีความอักขระพิเศษ) แล้วให้ subprocess จัดการ quoting เอง:
subprocess.run(["ls", user_filename]) # looks for a literal file named # "notes.txt; echo INJECTED" -- not found, no injection
subprocessสร้าง OS process จริง — รันในเครื่อง; ไม่มี process model ใน browser sandbox
secrets เทียบกับ random
หัวข้อที่มีชื่อว่า “secrets เทียบกับ random”random เป็นตัวสร้างสุ่มเทียมที่เร็วและทำซ้ำได้ (seed ได้) — เหมาะสำหรับ simulation เกม และการสับข้อมูลทดสอบ และไม่เหมาะโดยสิ้นเชิงสำหรับสิ่งที่เกี่ยวข้องกับความปลอดภัย เพราะ state ของมันสามารถถูกกู้คืนได้จาก output ที่มากพอ secrets ใช้แหล่งที่ปลอดภัยทางการเข้ารหัสของ OS และไม่มีการ seed เลย:
import random, secrets
random.seed(0)random.randint(1, 100) # 50 -- reproducible given the same seed, by designsecrets.token_hex(8) # e.g. 'e1535f914b8e7eb4' -- cryptographically unpredictablesecrets.choice(["a", "b", "c"])ใช้ secrets สำหรับ token, ลิงก์รีเซ็ตรหัสผ่าน, API key และ session ID — ห้ามใช้ random เด็ดขาด
decimal — เลขคณิตที่แม่นยำสำหรับเงิน
หัวข้อที่มีชื่อว่า “decimal — เลขคณิตที่แม่นยำสำหรับเงิน”floating-point float ไม่สามารถแทนเศษส่วนทศนิยมส่วนใหญ่ได้แม่นยำ ซึ่งเป็นหายนะสำหรับเงิน:
print(0.1 + 0.2) # 0.30000000000000004
from decimal import Decimalprint(Decimal("0.1") + Decimal("0.2")) # 0.3print(Decimal("19.99") * 3) # 59.97สร้าง Decimal จากสตริงเสมอ (Decimal("0.1")) ห้ามสร้างจาก float (Decimal(0.1)) — แบบหลังจะทำซ้ำความไม่แม่นยำของ float เองอย่างซื่อสัตย์แทนที่จะแก้มัน
enum — ค่าคงที่ที่มีชื่อและเป็น type จริง ๆ
หัวข้อที่มีชื่อว่า “enum — ค่าคงที่ที่มีชื่อและเป็น type จริง ๆ”string หรือ int ดิบสำหรับ status code รับค่าอะไรก็ได้ รวมถึงที่พิมพ์ผิด Enum คือชุดค่าที่มีชื่อและปิดตายพร้อมการตรวจ type:
from enum import Enum, auto, IntFlag
class Status(Enum): PENDING = auto() SHIPPED = auto() DELIVERED = auto()
print(Status.SHIPPED) # Status.SHIPPEDprint(Status.SHIPPED.name, Status.SHIPPED.value) # SHIPPED 2print(Status.SHIPPED == Status.SHIPPED) # True
class Perm(IntFlag): READ = 1 WRITE = 2 EXEC = 4
p = Perm.READ | Perm.WRITEprint(p, Perm.READ in p, Perm.EXEC in p) # Perm.READ|WRITE True FalseIntFlag รองรับการรวมแบบ bitwise ได้โดยตรง ทำให้มันเป็นเครื่องมือที่ถูกต้องสำหรับชุดสิทธิ์; Enum ธรรมดาไม่รองรับ | ระหว่าง member
อะไรมา อะไรไป
หัวข้อที่มีชื่อว่า “อะไรมา อะไรไป”standard library เคลื่อนไหวทั้งสองทิศทาง Python 3.13 เอาโมดูลที่ deprecate มานานออก 19 ตัว (PEP 594) —
cgi, telnetlib, crypt, imghdr, nntplib, pipes, uu และเพื่อน ๆ บวก lib2to3 โค้ดที่ import
โมดูลเหล่านี้ต้องหาตัวแทนจาก third-party หรือเขียนใหม่
สิ่งที่ควรรู้อีกฝั่งของบัญชี:
| สิ่งที่เพิ่มเข้ามา | เวอร์ชัน | ทำไมถึงสำคัญ |
|---|---|---|
itertools.batched(it, n) | 3.12 | แบ่งเป็นก้อนขนาดคงที่โดยไม่ต้องเขียนลูปเอง |
pathlib.Path.walk() | 3.12 | os.walk ที่ yield Path object |
copy.replace(obj, **changes) | 3.13 | dataclasses.replace แบบทั่วไป ผ่าน __replace__ |
dbm.sqlite3 | 3.13 | SQLite เป็น backend เริ่มต้นของ dbm แล้ว |
glob.translate() | 3.13 | wildcard ของ shell แปลงเป็น regular expression |
compression.zstd | 3.14 | Zstandard เชื่อมเข้ากับ tarfile, zipfile, shutil ด้วย |
pathlib.Path.copy() และ .move() | 3.14 | copy และ move โดยไม่ต้องพึ่ง shutil |
heapq.heapify_max() และเพื่อน ๆ | 3.14 | max-heap โดยไม่ต้องกลับเครื่องหมายทุกค่า |
functools.Placeholder | 3.14 | ข้าม positional argument เวลาสร้าง partial |
Decimal.from_number() | 3.14 | สร้างค่าแม่นยำจาก int หรือ float |
import copy, heapqfrom dataclasses import dataclassfrom functools import Placeholder, partialfrom itertools import batched
print(list(batched("ABCDEFG", 3)))
@dataclassclass Point: x: int y: int
print(copy.replace(Point(1, 2), y=99))
scores = [5, 1, 9, 3]heapq.heapify_max(scores)print("largest:", scores[0])
to_2dp = partial(round, Placeholder, 2)print(to_2dp(3.14159))Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR Naive["naive datetime -- no tzinfo, ambiguous"] -->|"tzinfo=timezone.utc"| Aware["aware datetime -- unambiguous point in time"] Naive -.->|"naive - aware"| Err["TypeError: can't mix naive and aware"] Aware -->|".astimezone(tz)"| Other["same instant, different tzinfo"] Str["ISO 8601 string"] -->|"fromisoformat()"| Aware
datetime.now() และ datetime.utcnow() ต่างก็คืนค่าแบบ naive — ความต่างมีแค่ว่าตัวเลข wall-clock ที่มันถือคืออะไร ทั้งคู่ไม่มีข้อมูล timezone ติดมาเลย จึงไม่สามารถเปรียบเทียบ ลบ หรือแปลงเทียบกับ aware datetime ได้อย่างปลอดภัย Python จะ raise TypeError แทนที่จะเดาเงียบ ๆ ทางแก้เหมือนกันเสมอ: ติด tzinfo ตั้งแต่จุดที่สร้าง datetime (datetime.now(timezone.utc) หรือ ZoneInfo(...) สำหรับภูมิภาคจริง) แล้วรักษาความเป็น aware ไว้ตั้งแต่นั้น
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
namedtuple / Counter / defaultdict | ไม่มี dependency เร็ว ผ่านการพิสูจน์แล้ว | ยืดหยุ่นน้อยกว่าคลาสเต็มรูปแบบเมื่อต้องการพฤติกรรม (เมธอด) |
datetime แบบ aware ทุกที่ | ไม่กำกวม เปรียบเทียบข้าม timezone ได้ปลอดภัย | ต้องใส่ argument เพิ่มหนึ่งตัวทุกจุดที่สร้าง ลืมง่าย |
Decimal สำหรับเงิน | เลขคณิตทศนิยมแม่นยำ ไม่มีปัญหาการปัดเศษของ float | ช้ากว่า float และเลขคณิตกับ float จะ raise TypeError |
secrets แทน random | token ที่ปลอดภัยทางการเข้ารหัส | seed ไม่ได้ — เสียความทำซ้ำได้ ซึ่งคือจุดประสงค์ |
subprocess.run([...]) แทน shell=True | ไม่มีช่องโหว่ shell-injection | เสียความสามารถของ shell (pipe, glob) ที่ต้องเขียนใหม่เองใน Python |
lru_cache | memoization ฟรี แค่ decorator เดียว | ทุก argument ต้อง hashable; maxsize=None แบบไม่จำกัดอาจกิน memory |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
datetime.now()ให้ ‘เวลา UTC ปัจจุบัน’” มันให้เวลาท้องถิ่นแบบ naiveที่ไม่มี timezone ติดมาเลย ไม่ใช่ทั้ง UTC หรือท้องถิ่นในแบบที่ Python ตรวจสอบได้ ใช้datetime.now(timezone.utc) - “
datetime.utcnow()คือเวอร์ชันที่ ‘ปลอดภัยแบบ UTC’ ของnow()” มันคืนตัวเลขของ UTC แต่เป็น object แบบ naive — แยกไม่ออกจาก naive datetime อื่น ๆ และถูก deprecate ตั้งแต่ 3.12 ด้วยเหตุผลนี้เอง - “
subprocess.run(cmd, shell=True)โอเคตราบใดที่cmd‘ดูเหมือน’ คำสั่งง่าย ๆ” สตริงใดก็ตามที่สร้างจาก input ของผู้ใช้หรือภายนอกแล้วส่งให้shell=Trueคือช่องโหว่ command injection ทันทีที่มี;,&&, backtick หรือ$(...) - “
random.choiceโอเคสำหรับสร้าง token รีเซ็ตรหัสผ่าน” output ของrandomทำซ้ำได้จาก internal state ของมันsecretsมีอยู่ก็เพื่อเหตุผลนี้ — “ดูเหมือนสุ่ม” กับ “ไม่สามารถคาดเดาได้ทางการเข้ารหัส” เป็นคุณสมบัติคนละอย่างกัน - “
0.1 + 0.2 == 0.3ใน Python เพราะ Python เป็นภาษาที่ ‘แม่นยำ’”floatของ Python คือ IEEE-754 double precision แบบเดียวกับที่ภาษากระแสหลักทุกตัวใช้ — ความไม่แม่นยำแบบเดียวกันเกิดขึ้นDecimal(สร้างจากสตริง) คือทางแก้ ไม่ใช่ Python คนละตัว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อโค้ดที่ถูกสร้างขึ้นซึ่งใช้โมดูลเหล่านี้ ให้ตรวจ:
datetime.now()หรือdatetime.utcnow()ที่ใช้ตรงไหนก็ตามที่ timestamp ถูกเก็บ เปรียบเทียบ หรือส่งข้ามขอบเขต timezone — ทั้งคู่คืน object แบบ naive;datetime.now(timezone.utc)มักเป็นสิ่งที่ตั้งใจจะเขียนจริง ๆsubprocess.run(cmd, shell=True)ที่ส่วนใดส่วนหนึ่งของcmdมาจาก input ผู้ใช้ ไฟล์ config หรือ service อื่น — เป็นช่องทาง command-injection จริง ไม่ใช่แค่ทฤษฎีopen(path)ที่ไม่มี argumentencoding=— ทำงานได้บนเครื่องผู้เขียน แต่พังทันทีที่ไฟล์มีเนื้อหาที่ไม่ใช่ ASCII บนแพลตฟอร์มที่มี encoding default ต่างออกไปfloatที่ใช้กับราคา ยอดเงินคงเหลือ หรือยอดรวมเงินใด ๆ —Decimalที่สร้างจากสตริงคือ type ที่ถูกต้องrandomที่ใช้สร้างอะไรก็ตามที่ต้องเดาไม่ได้ (token, รหัสผ่านชั่วคราว, session ID) — นั่นคือหน้าที่ของsecrets
🐛 หา bug ในโค้ดนี้
import subprocessfrom pathlib import Path
def export_report(filename: str, content: str) -> None: report_path = Path("/tmp/reports") / filename with open(report_path, "w") as f: f.write(content) # Compress the report using the user-supplied filename subprocess.run(f"gzip /tmp/reports/{filename}", shell=True)เฉลย
สอง bug: open(report_path, "w") ไม่มี encoding= ทำให้พฤติกรรมขึ้นอยู่กับ encoding ข้อความ default ของแพลตฟอร์มสำหรับ content ที่ไม่ใช่ ASCII แย่กว่านั้น subprocess.run(f"gzip /tmp/reports/{filename}", shell=True) สร้าง shell command string ตรงจาก filename — ผู้เรียกที่ส่ง filename="report.txt; rm -rf /tmp/reports" จะทำให้คำสั่งที่สองถูก shell รันด้วย ยืนยันแล้ว: subprocess.run("ls notes.txt; echo INJECTED", shell=True) พิมพ์ INJECTED จริง พิสูจน์ว่า shell ตีความ ; เป็นตัวแบ่งคำสั่ง แก้ทั้งสองจุด: open(report_path, "w", encoding="utf-8") และ subprocess.run(["gzip", str(report_path)]) — เป็น list ไม่มี shell ไม่มี injection