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

ทัวร์ Stdlib

Python standard library มีขนาดใหญ่ แต่มีโมดูลไม่กี่ตัวที่ปรากฏในแทบทุกโค้ดเบสจริงจัง บทนี้ให้บทนำที่รันได้จริงของห้าตัวคือ collections, itertools, datetime, json และ pathlib — บวก functools, decimal, enum, secrets และ subprocess


โมดูล collections ต่อยอด container ในตัวของ Python ด้วยเครื่องมือหลักสี่ตัวที่คุณจะใช้ทุกวัน

Counter นับ object ที่ hashable ได้และรองรับการคำนวณทางคณิตศาสตร์บนจำนวนนับ

from collections import Counter
c = 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 defaultdict
dd = defaultdict(list)
dd["fruits"].append("apple") # no KeyError on a fresh key

namedtuple สร้าง record type ที่เบาและ immutable พร้อมชื่อ field — อ่านง่ายกว่า tuple ธรรมดาและไม่มี overhead เพิ่มเมื่อเทียบกับ tuple ปกติ

from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4

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 itertools
list(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']

import collections
import 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])))

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 datetimes

datetime.now() ที่ไม่มี argument คืนเวลาท้องถิ่นแบบ naive — เป็นต้นตอของ bug บ่อยครั้งเมื่อค่านั้นถูกเก็บ เปรียบเทียบ หรือส่งข้าม timezone ให้ใส่ timezone เสมอ:

now_utc = dt.datetime.now(dt.timezone.utc) # aware, unambiguous

datetime.utcnow() ดูเหมือนจะแก้ปัญหาแต่ไม่ใช่ — มันคืน datetime แบบ naive ที่บังเอิญถือตัวเลขของ UTC และถูก deprecate แล้ว:

import warnings
warnings.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:00

datetime.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.dumps แปลง Python object เป็น JSON string; json.loads parse กลับ argument separators ช่วยตัด whitespace ออกเพื่อ output ที่กระชับ

import json
s = json.dumps({"key": "value"}, separators=(",", ":"))
# '{"key":"value"}'

json รู้จักแค่ชุด Python type ที่กำหนดตายตัวเท่านั้น — Decimal (type ที่ถูกต้องสำหรับเงิน ดูด้านล่าง) จะ raise TypeError เว้นแต่คุณจะให้ default= ซึ่งเป็นทางสำรองที่ถูกเรียกกับอะไรก็ตามที่มันไม่รู้จัก:

from dataclasses import dataclass, asdict
from decimal import Decimal
@dataclass
class 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": "กรุงเทพ"}'

import datetime
import json
# Fixed date for determinism
dt = 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 naive
aware = dt.replace(tzinfo=datetime.timezone.utc)
print("naive tzinfo:", dt.tzinfo, "| aware tzinfo:", aware.tzinfo)
try:
dt - aware
except TypeError as e:
print("TypeError:", e)
# json round-trip
obj = {"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_ascii
print(json.dumps({"note": "café"}))
print(json.dumps({"note": "café"}, ensure_ascii=False))

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 call
fib.cache_info() # CacheInfo(hits=28, misses=31, maxsize=128, currsize=31)

lru_cache ต้องการให้ทุก argument hashable — decorator cache ที่ AI สร้างบนฟังก์ชันที่รับ list จะล้มเหลวตอนเรียก ไม่ใช่ตอน decorate:

@lru_cache
def 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
@singledispatch
def describe(x):
return f"generic: {x!r}"
@describe.register
def _(x: int):
return f"int: {x}"
@describe.register
def _(x: str):
return f"str: {x!r}"
describe(5) # 'int: 5'
describe("hi") # "str: 'hi'"
describe(3.14) # 'generic: 3.14'

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) # .py
print(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.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 seconds

shell=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


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 design
secrets.token_hex(8) # e.g. 'e1535f914b8e7eb4' -- cryptographically unpredictable
secrets.choice(["a", "b", "c"])

ใช้ secrets สำหรับ token, ลิงก์รีเซ็ตรหัสผ่าน, API key และ session ID — ห้ามใช้ random เด็ดขาด


floating-point float ไม่สามารถแทนเศษส่วนทศนิยมส่วนใหญ่ได้แม่นยำ ซึ่งเป็นหายนะสำหรับเงิน:

print(0.1 + 0.2) # 0.30000000000000004
from decimal import Decimal
print(Decimal("0.1") + Decimal("0.2")) # 0.3
print(Decimal("19.99") * 3) # 59.97

สร้าง Decimal จากสตริงเสมอ (Decimal("0.1")) ห้ามสร้างจาก float (Decimal(0.1)) — แบบหลังจะทำซ้ำความไม่แม่นยำของ float เองอย่างซื่อสัตย์แทนที่จะแก้มัน


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.SHIPPED
print(Status.SHIPPED.name, Status.SHIPPED.value) # SHIPPED 2
print(Status.SHIPPED == Status.SHIPPED) # True
class Perm(IntFlag):
READ = 1
WRITE = 2
EXEC = 4
p = Perm.READ | Perm.WRITE
print(p, Perm.READ in p, Perm.EXEC in p) # Perm.READ|WRITE True False

IntFlag รองรับการรวมแบบ 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.12os.walk ที่ yield Path object
copy.replace(obj, **changes)3.13dataclasses.replace แบบทั่วไป ผ่าน __replace__
dbm.sqlite33.13SQLite เป็น backend เริ่มต้นของ dbm แล้ว
glob.translate()3.13wildcard ของ shell แปลงเป็น regular expression
compression.zstd3.14Zstandard เชื่อมเข้ากับ tarfile, zipfile, shutil ด้วย
pathlib.Path.copy() และ .move()3.14copy และ move โดยไม่ต้องพึ่ง shutil
heapq.heapify_max() และเพื่อน ๆ3.14max-heap โดยไม่ต้องกลับเครื่องหมายทุกค่า
functools.Placeholder3.14ข้าม positional argument เวลาสร้าง partial
Decimal.from_number()3.14สร้างค่าแม่นยำจาก int หรือ float
import copy, heapq
from dataclasses import dataclass
from functools import Placeholder, partial
from itertools import batched
print(list(batched("ABCDEFG", 3)))
@dataclass
class 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))
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: aware กับ naive คือความต่างระดับ type ไม่ใช่แค่ field ที่ขาดไป

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 แทน randomtoken ที่ปลอดภัยทางการเข้ารหัสseed ไม่ได้ — เสียความทำซ้ำได้ ซึ่งคือจุดประสงค์
subprocess.run([...]) แทน shell=Trueไม่มีช่องโหว่ shell-injectionเสียความสามารถของ shell (pipe, glob) ที่ต้องเขียนใหม่เองใน Python
lru_cachememoization ฟรี แค่ 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 คนละตัว

ก่อนเชื่อโค้ดที่ถูกสร้างขึ้นซึ่งใช้โมดูลเหล่านี้ ให้ตรวจ:

  • 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) ที่ไม่มี argument encoding= — ทำงานได้บนเครื่องผู้เขียน แต่พังทันทีที่ไฟล์มีเนื้อหาที่ไม่ใช่ ASCII บนแพลตฟอร์มที่มี encoding default ต่างออกไป
  • float ที่ใช้กับราคา ยอดเงินคงเหลือ หรือยอดรวมเงินใด ๆ — Decimal ที่สร้างจากสตริงคือ type ที่ถูกต้อง
  • random ที่ใช้สร้างอะไรก็ตามที่ต้องเดาไม่ได้ (token, รหัสผ่านชั่วคราว, session ID) — นั่นคือหน้าที่ของ secrets

🐛 หา bug ในโค้ดนี้

import subprocess
from 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

print(collections.Counter('aab')) ให้ผลอะไร?
ทำไมข้อมูลต้องถูก sort ก่อนส่งเข้า itertools.groupby?
เมธอดไหนของ datetime ที่ format วันที่เป็นสตริง?
เกิดอะไรขึ้น?
import datetime as dt
naive = dt.datetime(2026, 9, 22, 10, 0)
aware = dt.datetime(2026, 9, 22, 10, 0, tzinfo=dt.timezone.utc)
naive - aware
output จริงมีอะไรรวมอยู่ด้วย?
import subprocess
filename = "notes.txt; echo INJECTED"
subprocess.run(f"ls {filename}", shell=True)
ทำไมใช้ Decimal("0.1") แทน Decimal(0.1)?
ทำไม secrets.token_hex() ถึงดีกว่า random.getrandbits() สำหรับ token รีเซ็ตรหัสผ่าน?