Comprehensions
Comprehension คืออะไร?
หัวข้อที่มีชื่อว่า “Comprehension คืออะไร?”comprehension คือนิพจน์ที่กระชับและอ่านง่าย ใช้สร้าง collection ใหม่โดยการแปลงหรือกรอง iterable Python มีสามรูปแบบ: list, dict และ set comprehension บวกกับนิพจน์ที่คล้ายกันอีกแบบที่สร้าง lazy iterator แทน collection — generator expression
List comprehensions
หัวข้อที่มีชื่อว่า “List comprehensions”รูปแบบทั่วไปคือ:
[expression for item in iterable][expression for item in iterable if condition]squares = [x**2 for x in range(1, 8)]# [1, 4, 9, 16, 25, 36, 49]
evens = [x for x in range(20) if x % 2 == 0]# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]ทั้งสองตัวอย่างเทียบเท่ากับ for loop ที่ append ลง list แต่กระชับกว่าและมักเร็วกว่า ทั้งสองแบบ compile เป็น bytecode ที่ interpreter ตัวเดียวกันรัน (comprehension ไม่ใช่ “โค้ด C”) ที่เร็วกว่ามาจากสองเรื่อง: (1) comprehension ใช้ opcode LIST_APPEND โดยตรง แทนที่จะต้อง LOAD_ATTR + CALL เพื่อค้นหาและเรียก .append ทุกรอบแบบ loop และ (2) ตั้งแต่ Python 3.12 (PEP 709) comprehension ถูก inline เข้า frame เดียวกันกับที่ล้อมรอบ ไม่ต้อง compile เป็น function call แยกต่างหาก
import dis
def comp(): return [x**2 for x in range(5)]
def loop(): result = [] for x in range(5): result.append(x**2) return resultopcode ของ inner loop จาก dis.dis(comp) และ dis.dis(loop) บน 3.14 ที่รันในเครื่องจริง ยืนยันเรื่องนี้:
comp(): FOR_ITER -> STORE_FAST_LOAD_FAST (x, x) -> LOAD_SMALL_INT 2 -> BINARY_OP 8 (**) -> LIST_APPEND 2 -> JUMP_BACKWARD
loop(): FOR_ITER -> STORE_FAST (x) -> LOAD_FAST_BORROW (result) -> LOAD_ATTR (append + NULL|self) -> LOAD_FAST_BORROW (x) -> LOAD_SMALL_INT 2 -> BINARY_OP 8 (**) -> CALL 1 -> POP_TOP -> JUMP_BACKWARDScope: ตัวแปร loop ไม่รั่วออกมา
หัวข้อที่มีชื่อว่า “Scope: ตัวแปร loop ไม่รั่วออกมา”ตั้งแต่ PEP 709 (Python 3.12) ได้ทำให้เป็นทางการในสิ่งที่ถูกต้องอยู่แล้วสำหรับพฤติกรรม runtime ตั้งแต่ Python 3: ตัวแปร for ของ comprehension อยู่ใน scope ที่ซ่อนของตัวเอง มันไม่เคยเขียนทับตัวแปรชื่อเดียวกันใน function หรือ module ที่ล้อมรอบเลย:
x = 'outer'squares = [x for x in range(5)]print(squares) # [0, 1, 2, 3, 4]print(x) # 'outer' — completely unaffected by the comprehension's own xนี่คือการเปลี่ยนพฤติกรรมจริงจาก Python 2 มาเป็น 3: ใน Python 2 ตัวแปร loop ของ list comprehension รั่ว เข้า scope ที่ล้อมรอบได้ ถ้าเคยอ่านโค้ดเก่าที่พึ่งพาค่าสุดท้ายของตัวแปร loop ให้รอดหลัง comprehension โค้ดนั้นเขียนไว้สำหรับ Python 2 และต้องใช้ for loop แบบชัดเจน (หรือ walrus :=) ภายใต้ Python 3
Nested comprehensions
หัวข้อที่มีชื่อว่า “Nested comprehensions”สามารถซ้อน for หลายตัวเพื่อวนซ้ำข้าม combination
ประโยค for อ่านจากซ้ายไปขวา — for ซ้ายสุดเป็น outer loop:
pairs = [ (x, y) for x in range(1, 4) for y in range(1, 4) if x != y]# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]การอ่านมันเป็น for statement ที่ซ้อนกันเขียนบนบรรทัดเดียว ทำให้ลำดับชัดเจนไม่คลุมเครือ: ประโยค for แรกกลายเป็น loop ชั้นนอกสุด เหมือนกับ for x in range(1, 4): for y in range(1, 4): ... เป๊ะ ๆ
Generator expression: lazy และประหยัดหน่วยความจำ
หัวข้อที่มีชื่อว่า “Generator expression: lazy และประหยัดหน่วยความจำ”เปลี่ยน [] เป็น () นิพจน์คล้าย comprehension จะกลายเป็น generator expression — มันสร้างค่าทีละตัวตามที่ถูกขอ แทนที่จะสร้าง collection ทั้งหมดในหน่วยความจำล่วงหน้า:
import syslist_version = [n * n for n in range(100_000)]gen_version = (n * n for n in range(100_000))print(sys.getsizeof(list_version)) # a large number of bytes — every element is materialisedprint(sys.getsizeof(gen_version)) # a small, constant number of bytes — no elements exist yetต้นทุนของความ lazy นี้: generator จะหมดสภาพหลังวนครบหนึ่งรอบ เรียก sum() บนมันสองครั้งจะไม่ได้คำตอบเดิมสองครั้ง:
gen = (n for n in range(5))print(sum(gen)) # 10print(sum(gen)) # 0 — the generator is already exhausted, nothing left to sumใช้ generator expression เมื่อต้องการวนซ้ำแค่ครั้งเดียวกับ sequence ขนาดใหญ่หรือไม่จำกัด ใช้ list (หรือ set/dict) comprehension เมื่อต้องวนซ้ำมากกว่าหนึ่งครั้ง, index เข้าไปในผลลัพธ์, หรือตรวจสอบความยาวของมัน
Filter เทียบกับ transform: if clause เทียบ conditional expression
หัวข้อที่มีชื่อว่า “Filter เทียบกับ transform: if clause เทียบ conditional expression”สองอย่างนี้หน้าตาคล้ายกันแต่ทำงานต่างกัน if clause หลัง for กรองว่า item ไหนจะถูกเก็บไว้เลย:
nums = [1, 2, 3, 4, 5, 6]filtered = [n for n in nums if n % 2 == 0]# [2, 4, 6] — odd numbers are dropped entirelyconditional expression (a if condition else b) ก่อน for เก็บทุก item ไว้ แต่แปลงค่าต่างกันไปตามเงื่อนไข:
transformed = [n if n % 2 == 0 else -n for n in nums]# [-1, 2, -3, 4, -5, 6] — every element survives, some are negatedทั้งสองแบบใช้ร่วมกันได้: กรองด้วย if หลัง for แปลงด้วย if/else ในนิพจน์ก่อน for:
both = [n * 10 for n in nums if n % 2 == 0]# [20, 40, 60] — filtered AND transformedDict comprehensions
หัวข้อที่มีชื่อว่า “Dict comprehensions”ใช้ {} และ syntax key: value
word_len = {word: len(word) for word in ['apple', 'banana', 'cherry']}# {'apple': 5, 'banana': 6, 'cherry': 6}Dict comprehension เป็นวิธีที่สะอาดสำหรับการกลับ mapping, กรอง entry หรือแปลง value:
prices = {'apple': 1.5, 'banana': 0.75, 'cherry': 3.0}expensive = {k: v for k, v in prices.items() if v >= 1.5}# {'apple': 1.5, 'cherry': 3.0}Set comprehensions
หัวข้อที่มีชื่อว่า “Set comprehensions”Syntax เหมือน list comprehension แต่ใช้ {}
ค่าซ้ำถูกลบออกอัตโนมัติ
unique_lens = {len(word) for word in ['apple', 'banana', 'cherry', 'date']}# {4, 5, 6} (order not guaranteed)เมื่อไหร่ไม่ควรใช้ comprehension: side effect
หัวข้อที่มีชื่อว่า “เมื่อไหร่ไม่ควรใช้ comprehension: side effect”หน้าที่ทั้งหมดของ comprehension คือสร้าง collection ถ้า “ค่า” ของแต่ละนิพจน์ถูกทิ้งไป และเหตุผลเดียวที่เขียนมันคือ side effect (print, append ลง structure อื่น, เขียนไฟล์) comprehension ก็ยังคงสร้าง — แล้วทิ้งทันที — list ของสิ่งที่ side effect นั้นคืนค่ามา:
wasted = [print(f'item {n}') for n in range(3)]# item 0# item 1# item 2print(wasted) # [None, None, None] — a throwaway list nobody asked forprint() คืนค่า None ดังนั้น wasted คือ list ของ None สามตัวที่ไม่มีเหตุผลจะมีอยู่ for loop ธรรมดาสื่อ intent ตรงไปตรงมาและไม่ allocate อะไรเพิ่มเลย:
for n in range(3): print(f'item {n}')ตัวอย่างรันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างรันได้”# --- list comprehension ---squares = [x**2 for x in range(1, 8)]print('squares:', squares)
evens = [x for x in range(20) if x % 2 == 0]print('evens:', evens)
# --- nested comprehension ---pairs = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]print('pairs:', pairs)
# --- dict comprehension ---word_len = {word: len(word) for word in ['apple', 'banana', 'cherry']}print('word lengths:', word_len)
# --- set comprehension ---unique_lens = {len(word) for word in ['apple', 'banana', 'cherry', 'date']}print('unique lengths:', sorted(unique_lens))Loading Python runtime (first run only)…
playground ด้านล่างคือหัวข้อความลึกด้านบนที่รันจริง — scope ไม่รั่ว, หน่วยความจำของ generator เทียบ list, การวนรอบที่สองของ generator ที่หมดสภาพแล้ว, filter เทียบ transform, ลำดับ nested, และ comprehension ที่ถูกใช้ผิดเพื่อ side effect:
import sys
# --- comprehension scope does not leak (Python 3) ---x = 'outer'squares = [x for x in range(5)]print('squares:', squares)print('x after list comprehension:', x)
# --- generator expression: lazy, tiny memory footprint ---list_version = [n * n for n in range(100_000)]gen_version = (n * n for n in range(100_000))print('list size (bytes):', sys.getsizeof(list_version))print('generator size (bytes):', sys.getsizeof(gen_version))print('sum from generator:', sum(gen_version))print('sum from exhausted generator (again):', sum(gen_version))
# --- filter (if clause) vs conditional expression (if/else) ---nums = [1, 2, 3, 4, 5, 6]filtered = [n for n in nums if n % 2 == 0]print('filter only (keeps some):', filtered)
transformed = [n if n % 2 == 0 else -n for n in nums]print('conditional expr (keeps all, transforms):', transformed)
# combining both: filter AND transformboth = [n * 10 for n in nums if n % 2 == 0]print('filter + transform:', both)
# --- nested comprehension evaluation order ---pairs = [(x, y) for x in range(1, 3) for y in ['a', 'b']]print('nested order (x outer, y inner):', pairs)
# --- when not to use a comprehension: side effects ---# This "works" but builds and immediately discards a list of Nones just# to run print() as a side effect -- wasteful and confusing to read.wasted = [print(f'item {n}') for n in range(3)]print('comprehension used for side effects returns:', wasted)Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR L["list comprehension: n*n for n in range(N)"] --> LM["builds every element now<br/>memory grows with N"] G["generator expression: n*n for n in range(N)"] --> GM["builds nothing yet<br/>constant, tiny memory"] GM --> Next["next value computed only when pulled<br/>by for, sum, next, etc."] Next --> Done["exhausted after one full pass<br/>a second pass yields nothing"]
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| list/dict/set comprehension | นิพจน์เดียวบอกว่า “สร้างสิ่งนี้จากสิ่งนั้น”; มักเร็วกว่า loop ที่ใช้ .append() | สร้าง collection ทั้งหมดในหน่วยความจำทันที |
generator expression (...) | หน่วยความจำแทบเป็นศูนย์ สร้างค่าตามที่ถูกขอ | ผ่านได้ครั้งเดียว — หมดสภาพหลังวนครบหนึ่งรอบ index หรือ len() ไม่ได้ |
if clause (filter) | ตัด item ที่ไม่ต้องการออกทั้งหมด | เปลี่ยนความยาวของผลลัพธ์ — สับสนกับ transform ได้ง่าย |
if/else expression (transform) | เก็บทุก item ไว้ เปลี่ยนแค่ค่า | ไม่กรอง — ทุก item ที่ป้อนเข้ายังปรากฏในผลลัพธ์เสมอ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “ตัวแปร loop ของ comprehension รั่วเข้า scope ที่ล้อมรอบ” — ความจริง: ใน Python 3 ตัวแปร
forของ comprehension เป็น local ของ comprehension เอง ตัวแปรชื่อเดียวกันข้างนอกไม่ถูกแตะต้อง - “
(x for x in ...)กับ[x for x in ...]เหมือนกันแค่วงเล็บต่างกัน” — ความจริง: รูปแบบวงเล็บกลมคือ generator expression — lazy, ผ่านได้ครั้งเดียว และใช้หน่วยความจำเกือบคงที่ ต่างจาก list ที่มันดูคล้ายกันผิวเผิน - “comprehension เร็วเพราะรันด้วย C ต่างจาก
forloop” — ความจริง: ทั้งสองแบบ compile เป็น Python bytecode ที่ interpreter ตัวเดียวกันรัน ความเร็วที่เพิ่มมาจาก opcodeLIST_APPENDเฉพาะทางและ (3.12+) การ inline frame ไม่ใช่จาก execution engine ที่ต่างกัน - “
[x for x in items if cond]กับ[x if cond else default for x in items]ทำงานเหมือนกัน” — ความจริง: อันแรกกรอง (ผลลัพธ์สั้นกว่าitemsได้) อันที่สองแปลงทุก item และไม่เคยเปลี่ยนความยาวของผลลัพธ์ - “
sum()(หรือวนซ้ำ) generator ได้มากกว่าหนึ่งครั้งเหมือน list” — ความจริง: generator เป็น iterator แบบใช้ครั้งเดียว พอหมดสภาพแล้ว การผ่านครั้งต่อไปจะไม่ได้อะไรเลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- โค้ดสร้าง generator expression แล้วส่งให้มากกว่าหนึ่ง function ที่แต่ละตัวบริโภคมันจนหมด (เช่น
sum(gen)ตามด้วยmax(gen), หรือสองforloop แยกกันบนgenเดียวกัน) หรือเปล่า? มีแค่ผู้บริโภครายแรกเท่านั้นที่เห็นค่าใด ๆ - comprehension มีจุดประสงค์เดียวคือ side effect หรือเปล่า —
[fn(x) for x in items]ที่ผลลัพธ์ไม่เคยถูก assign หรือ assign แล้วไม่เคยถูกใช้? นั่นควรเป็นforloop ธรรมดา range()ขนาดใหญ่หรือการอ่านไฟล์ถูกครอบด้วย[...](list comprehension) ทั้งที่ต้องการแค่ผ่านเชิงเส้นครั้งเดียวหรือเปล่า? generator expression (หรือ generator เปล่า ๆ ไม่ต้องมี comprehension เลยสำหรับsum(x for x in ...)) จะใช้หน่วยความจำน้อยกว่ามาก- conditional expression แบบ
if/elseถูกใช้ในที่ที่โค้ดต้องการกรอง (ตัด) item ออก หรือiffilter clause ถูกใช้ในที่ที่โค้ดต้องการแปลงทุก item หรือเปล่า? ตรวจว่าความยาวของผลลัพธ์ควรตรงกับความยาวของ input หรือไม่ - ลำดับของ nested comprehension (
for x ... for y ...) ตรงกับ outer/inner loop ที่ผู้เขียนตั้งใจไว้ หรือผู้เขียนสมมติว่ามันอ่านจากขวาไปซ้ายหรือเปล่า?
🐛 หา bug ในโค้ดนี้
def process(data): filtered = (x for x in data if x > 0) total = sum(filtered) count = sum(1 for _ in filtered) return total, count
print(process([1, -2, 3, -4, 5]))เฉลย
filtered คือ generator expression ไม่ใช่ list — มันวนซ้ำได้แค่ครั้งเดียว sum(filtered) บริโภคมันจนหมดตอนคำนวณ total ดังนั้นพอถึงเวลา sum(1 for _ in filtered) รัน filtered ก็หมดสภาพไปแล้วและให้ item เป็นศูนย์ ตรวจสอบแล้วบน Python 3.14: โค้ดนี้พิมพ์ (9, 0) ไม่ใช่ (9, 3) — ไม่มี exception เกิดขึ้นเลย ซึ่งเป็นสิ่งที่ทำให้ bug นี้อันตราย มันล้มเหลวแบบเงียบ ๆ วิธีแก้คือทำให้ค่าที่กรองแล้วเป็นรูปธรรมครั้งเดียว — filtered = [x for x in data if x > 0] — เพื่อให้ทั้ง sum(filtered) และ len(filtered) (ซึ่งแทนที่ sum(1 for _ in ...) ตัวที่สองไปเลย) วนซ้ำมันได้อย่างอิสระต่อกัน