Control Flow
Conditional: if / elif / else
หัวข้อที่มีชื่อว่า “Conditional: if / elif / else”Python ใช้การเยื้อง (indentation) แทนวงเล็บปีกกาสำหรับ conditional expression ใดก็ตามที่ประเมินเป็น truthy หรือ falsy ใช้เป็นเงื่อนไขได้
score = 85
if score >= 90: grade = "A"elif score >= 80: grade = "B"elif score >= 70: grade = "C"else: grade = "F"
print(grade) # BStructural pattern matching: match
หัวข้อที่มีชื่อว่า “Structural pattern matching: match”ตั้งแต่ Python 3.10, match/case จับคู่ รูปร่าง ของค่า ไม่ใช่แค่ความเท่ากัน — value, การตรวจ type, sequence pattern, หรือ mapping pattern ที่ลองไล่จากบนลงล่าง โดยมี _ เป็นตัวจับทุกอย่างที่เหลือ:
def describe(value): match value: case 0: return "zero" case int() if value < 0: return "negative int" case [x, y]: return f"pair: {x}, {y}" case {"kind": kind}: return f"dict with kind={kind}" case _: return "something else"
print(describe(0)) # zeroprint(describe(-5)) # negative intprint(describe([1, 2])) # pair: 1, 2print(describe({"kind": "widget"})) # dict with kind=widgetcase [x, y] ยัง bind x กับ y ให้ด้วยถ้า pattern ตรงกัน — นี่คือ destructuring ไม่ใช่แค่การเปรียบเทียบ ซึ่งเป็นเหตุผลที่ match แทนที่ chain ของ isinstance/index check ได้กระชับกว่า if/elif chain ธรรมดามาก
for loop เหนือ iterable
หัวข้อที่มีชื่อว่า “for loop เหนือ iterable”for loop ของ Python iterate เหนือ iterable ใดก็ได้ — list, string, range, file, dict, หรือ object ที่ implement __iter__ เอง
fruits = ["apple", "banana", "cherry"]for fruit in fruits: print(fruit)
# Iterate over characters in a stringfor ch in "hello": print(ch)ใช้ enumerate() เมื่อต้องการทั้ง index และ value:
for i, fruit in enumerate(fruits, start=1): print(f"{i}. {fruit}")for ทำงานยังไงจริง ๆ: iter() และ next()
หัวข้อที่มีชื่อว่า “for ทำงานยังไงจริง ๆ: iter() และ next()”for item in iterable: เป็นแค่ syntactic sugar เบื้องหลัง Python เรียก iter(iterable) ครั้งเดียวเพื่อได้ iterator แล้วเรียก next() กับมันซ้ำ ๆ จนกว่ามันจะ raise StopIteration ซึ่ง loop จับไว้เงียบ ๆ เพื่อจบการวนซ้ำ:
nums = [10, 20, 30]it = iter(nums)print(next(it)) # 10print(next(it)) # 20print(next(it)) # 30next(it) # StopIteration -- iterator exhaustedการรู้เรื่องนี้อธิบายพฤติกรรมที่ดูเหมือนเวทมนตร์ได้: iterator จำตำแหน่งของตัวเองระหว่างการเรียก next() (มัน มี state) และเมื่อหมดแล้วก็หมดตลอดไป — เรียก iter() ซ้ำกับ iterator object ตัวเดิมก็แค่คืนตัวมันเอง ยังว่างเปล่าเหมือนเดิม for loop ใหม่เหนือ iterable เดิม (อย่าง list) ทำงานได้อีกครั้งก็เพราะ iter(a_list) สร้าง iterator ใหม่ทุกครั้งเท่านั้น ซึ่งไม่จริงสำหรับ generator ที่เป็น iterator ของตัวมันเอง
การแก้ list ขณะ iterate มันอยู่
หัวข้อที่มีชื่อว่า “การแก้ list ขณะ iterate มันอยู่”for loop ติดตามตำแหน่งเป็นตัวเลขใน sequence ไม่ใช่ “item ที่ยังไม่ได้เยี่ยมชม” การลบ item ออกจาก list ที่กำลัง iterate อยู่ทำให้ item ที่เหลือทุกตัวเลื่อนมาข้างหน้าหนึ่งช่อง ซึ่งตัวนับตำแหน่งของ loop ไม่รู้เรื่องนี้ด้วย มันจึงข้าม item ที่เลื่อนเข้ามาในตำแหน่งที่มันผ่านไปแล้วแบบเงียบ ๆ:
values = [1, 2, 2, 3, 4, 5]for n in values: if n % 2 == 0: values.remove(n)print(values) # [1, 2, 3, 5] -- the second 2 survived; 4 was removed insteadloop เยี่ยม index 0 (1), index 1 (2 ถูกลบ — list หดตัวและทุกอย่างเลื่อนซ้าย) แล้วก็ index 2 — แต่ element ที่อยู่ตำแหน่ง index 2 ตอนนี้คือ 2 ตัวที่สอง ซึ่งเคยอยู่ index 3 loop “ผ่าน” ตำแหน่งนั้นไปแล้วและไม่กลับมาเยี่ยมอีก มันจึงรอด ในขณะที่ 4 ถูกลบไปโดยบังเอิญของตำแหน่ง ทางแก้คือ iterate เหนือ copy — for n in values.copy(): หรือ for n in list(values): — หรือสร้าง list ใหม่ด้วย comprehension แทนการแก้ในที่: values = [n for n in values if n % 2 != 0]
range()
หัวข้อที่มีชื่อว่า “range()”range(stop), range(start, stop), และ range(start, stop, step) สร้างลำดับจำนวนเต็มโดยไม่สร้าง list ในหน่วยความจำ
for i in range(5): # 0, 1, 2, 3, 4 print(i)
for i in range(2, 10, 2): # 2, 4, 6, 8 print(i)range เป็น lazy — range object เก็บแค่ start, stop, step แล้วคำนวณแต่ละค่าตามที่ถูกขอ แทนที่จะสร้างจำนวนเต็มทุกตัวไว้ล่วงหน้า:
import sys
r = range(1_000_000)print(type(r).__name__) # rangeprint(sys.getsizeof(r)) # 48 -- three integers, regardless of lengthprint(sys.getsizeof(list(r))) # 8000056 -- one million actual int objectsrange ของตัวเลขพันล้านตัวก็มีต้นทุน 48 byte เท่ากับ range ของตัวเลขสามตัว นี่คือเหตุผลที่ for i in range(len(items)): ประหยัดหน่วยความจำแม้ items จะใหญ่มาก และเป็นเหตุผลที่ควรใช้ range (หรือ generator) แทนการสร้าง list เพียงเพื่อ loop ผ่านมันครั้งเดียว
while loop
หัวข้อที่มีชื่อว่า “while loop”while ทำซ้ำตราบใดที่เงื่อนไขเป็น truthy
count = 0while count < 3: print(count) count += 1break, continue, และ else ของ loop
หัวข้อที่มีชื่อว่า “break, continue, และ else ของ loop”breakออกจาก loop ที่อยู่ในสุดทันทีcontinueข้ามส่วนที่เหลือของรอบปัจจุบันแล้วไปรอบถัดไป- clause
elseของ loop รันก็ต่อเมื่อ loop จบโดยไม่โดนbreak— กฎนี้ใช้เหมือนกันทั้งfor...elseและwhile...else
for n in [1, 3, 5, 7]: if n % 2 == 0: print("found even") breakelse: print("no even number found") # this runsbreak ออกแค่ loop ที่อยู่ ในสุด เท่านั้น — Python ไม่มี labeled break แบบภาษาอื่น การจะออกจาก loop ที่ซ้อนกันสองชั้นต้องใช้ตัวแปร sentinel ที่เช็คหลัง inner loop หรือย้ายทั้งหมดเข้า function แล้วใช้ return:
found = Nonefor i in range(3): for j in range(3): if i * j == 4: found = (i, j) break # only exits the inner `for j` loop if found: break # this second break exits the outer loopprint(found) # (2, 2)Chained comparison
หัวข้อที่มีชื่อว่า “Chained comparison”Python ให้ chain การเปรียบเทียบได้: a < b < c หมายถึง a < b and b < c โดยประเมิน b แค่ครั้งเดียว วิธีนี้อ่านเป็นธรรมชาติสำหรับการเช็คช่วงตัวเลข — แต่กฎ chain เดียวกันนี้ใช้กับ operator เปรียบเทียบ ใด ๆ ที่ผสมกัน รวมถึง in ด้วย ซึ่งเป็นจุดที่มันหยุดดูเหมือนคณิตศาสตร์ธรรมดา:
x, y = 10, 5lst = [True, False]
print(x < y in lst) # (x < y) and (y in lst) -> False and False -> Falseprint((x < y) in lst) # bool(x < y) tested for membership -> False in lst -> Truea < b in c ไม่ใช่ (a < b) in c — มันคือ a < b and b in c เพราะ Python ถือว่า in เป็นแค่ comparison operator อีกตัวที่ chain ได้เหมือน < ถ้าคุณตั้งใจถามว่า “ผลลัพธ์ boolean ของ a < b เป็นสมาชิกของ c ไหม” ต้องใส่วงเล็บ (a < b) in c ให้ชัดเจน
// และ % กับจำนวนลบ
หัวข้อที่มีชื่อว่า “// และ % กับจำนวนลบ”Floor division (//) ปัดเศษไปทาง ลบอนันต์เสมอ ไม่ใช่เข้าหาศูนย์ — และ % ถูกนิยามให้ a == (a // b) * b + (a % b) เป็นจริงเสมอ:
print(-7 // 2, -7 % 2) # -4 1print(7 // -2, 7 % -2) # -4 -1-7 // 2 คือ -4 ไม่ใช่ -3 — ภาษาส่วนใหญ่ที่ใช้ truncating division แบบ C จะได้ -3 ในกรณีนี้ ซึ่งเป็นต้นเหตุ off-by-one bug ที่พบบ่อยเมื่อย้ายโค้ดข้ามภาษา เครื่องหมายของผลลัพธ์ % ตรงกับเครื่องหมายของ ตัวหาร เสมอ ไม่ใช่ตัวตั้ง
Walrus operator :=
หัวข้อที่มีชื่อว่า “Walrus operator :=”เริ่มใน Python 3.8, := กำหนดค่าและประเมินเป็นค่านั้นใน expression เดียวกัน
มีประโยชน์มากที่สุดใน while และ if condition เพื่อไม่ต้องเขียน expression ซ้ำ
data = [4, 7, 2, 9, 1]if (largest := max(data)) > 5: print(f"Largest value {largest} exceeds 5")Late-binding closure ใน loop
หัวข้อที่มีชื่อว่า “Late-binding closure ใน loop”lambda (หรือ nested def) ที่สร้างใน loop ไม่ได้จับค่าของตัวแปร loop ตอนที่ถูกสร้าง — มันจับ ชื่อ ที่จะถูกค้นหาใหม่ทุกครั้งที่ function ถูกเรียกจริง พอถึงเวลาที่ closure ไหนถูกเรียก loop ก็จบไปแล้วและชื่อนั้นมีค่าสุดท้าย:
funcs = [lambda: i for i in range(3)]print([f() for f in funcs]) # [2, 2, 2] -- every closure reads the SAME ilambda ทั้งสามตัวใช้ enclosing scope เดียวกันและตัวแปร i ตัวเดียว ไม่มีตัวไหนเก็บ 0, 1, หรือ 2 เลย — ทุกตัว lookup i ตอนถูกเรียก ซึ่งตอนนั้น loop จบแล้วและ i เป็น 2 ทางแก้ที่ idiomatic คือ bind ค่าในแต่ละรอบเป็น default argument ซึ่งถูกประเมินครั้งเดียวทันที ตอนกำหนด function:
funcs_fixed = [lambda i=i: i for i in range(3)]print([f() for f in funcs_fixed]) # [0, 1, 2] -- each default was bound at creation timeDemo ที่รันได้จริง
หัวข้อที่มีชื่อว่า “Demo ที่รันได้จริง”score = 85if score >= 90: grade = "A"elif score >= 80: grade = "B"elif score >= 70: grade = "C"else: grade = "F"print(f"Score {score} -> Grade {grade}")
total = 0for i in range(1, 6): total += iprint(f"Sum 1..5 = {total}")
numbers = [1, 3, 5, 7]for n in numbers: if n % 2 == 0: print(f"Found even: {n}") breakelse: print("No even numbers found")
count = 0while True: count += 1 if count % 2 == 0: continue if count > 7: break print(f"odd: {count}")
data = [4, 7, 2, 9, 1]if (largest := max(data)) > 5: print(f"Largest value {largest} exceeds 5")Loading Python runtime (first run only)…
Playground ที่สองเป็นทัวร์ของกับดักด้านบน: iterator protocol, การแก้ list ขณะ iterate, range แบบ lazy, กับดัก chained comparison, floor division กับจำนวนลบ, break ใน nested loop, และ late-binding closure
import sys
# 1. match (structural pattern matching)def describe(value): match value: case 0: return "zero" case int() if value < 0: return "negative int" case [x, y]: return f"pair: {x}, {y}" case {"kind": kind}: return f"dict with kind={kind}" case _: return "something else"
print(describe(0))print(describe(-5))print(describe([1, 2]))print(describe({"kind": "widget"}))print(describe("hello"))
# 2. for runs on iter()/next().nums = [10, 20, 30]it = iter(nums)print(next(it))print(next(it))print(next(it))try: next(it)except StopIteration: print("StopIteration -- iterator exhausted")
# 3. Mutating a list while iterating over it.values = [1, 2, 2, 3, 4, 5]for n in values: if n % 2 == 0: values.remove(n)print("after removing evens while iterating:", values)
# 4. range() is lazy.r = range(1_000_000)print("type:", type(r).__name__)print("range size:", sys.getsizeof(r))print("list size:", sys.getsizeof(list(r)))
# 5. Chained comparison and the "a < b in c" trap.x, y = 10, 5lst = [True, False]print("chained x < y in lst:", x < y in lst) # (x < y) and (y in lst)print("(x < y) in lst:", (x < y) in lst) # boolean tested for membership
# 6. // and % with negative numbers.print(-7 // 2, -7 % 2)print(7 // -2, 7 % -2)
# 7. while / else.count = 0while count < 3: print("while count:", count) count += 1else: print("while/else: loop finished without break")
# 8. break only exits the innermost loop.found = Nonefor i in range(3): for j in range(3): if i * j == 4: found = (i, j) break if found: breakprint("found:", found)
# 9. Late-binding closures in a loop.funcs = [lambda: i for i in range(3)]print("late binding:", [f() for f in funcs])
funcs_fixed = [lambda i=i: i for i in range(3)]print("fixed with default arg:", [f() for f in funcs_fixed])Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”for item in iterable: คือ state machine เล็ก ๆ ไม่ใช่การกวาดผ่าน “item ทั้งหมด” แบบเวทมนตร์ Python ขอ iterator จาก iterable ครั้งเดียว (iter()) แล้วขอค่าถัดไปจาก iterator นั้นซ้ำ ๆ (next()) จนกว่ามันจะส่งสัญญาณ StopIteration มีแค่ iterator เท่านั้นที่ติดตามตำแหน่ง — ตัว iterable เองไม่เคยถูกถามว่า “อะไรอยู่หลัง item ที่ให้ไปครั้งล่าสุด”
flowchart LR
A["iter(iterable)"] --> B["iterator object"]
B --> C{"next(iterator)"}
C -->|"value"| D["run loop body"]
D --> C
C -->|"StopIteration"| E["loop ends"] ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
for สร้างจาก iter()/next() | ใช้ได้เหมือนกันกับ list, file, generator, object ที่กำหนดเอง | การแก้ container ขณะ iterate ทำให้ข้ามหรือทำซ้ำ element แบบเงียบ ๆ |
range แบบ lazy | หน่วยความจำ O(1) ไม่ว่า range จะใหญ่แค่ไหน | ไม่ใช่ list — ไม่มี .append() และการ slice/index ต้องคำนวณใหม่แทนที่จะดูจาก storage |
| Chained comparison | เขียนเช็คช่วงตัวเลขกระชับ (0 <= x < 10) | operator เปรียบเทียบใด ๆ chain ได้ รวมถึง in ซึ่งอ่านต่างจากที่คาดไว้ |
else ของ loop | แทนตัวแปร flag “found” ด้วยมือ | เข้าใจผิดง่ายว่า “รันเมื่อ loop วนศูนย์รอบ” — ไม่ใช่ความหมายนั้น |
match structural pattern | destructure และจับคู่รูปร่างในที่เดียว แทน isinstance/index check ที่ซ้อนกัน | syntax ใหม่ (3.10+) ตัวแปรใน case bind เสมอ ไม่เคยเทียบกับค่าตัวแปรที่มีอยู่แล้วโดยบังเอิญ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ลบ item ออกจาก list ขณะ loop ด้วย
forได้อย่างปลอดภัย” ไม่จริง — การลบเลื่อน element ที่เหลือเข้าไปในตำแหน่งที่เยี่ยมชมไปแล้ว ข้ามบางตัวไปแบบเงียบ ๆ ให้ iterate เหนือ copy หรือสร้าง list ใหม่แทน - “
range(1_000_000)จองจำนวนเต็มหนึ่งล้านตัวทันที” ไม่จริง — range object เก็บแค่ start/stop/step แล้วคำนวณค่าแบบ lazy มีแค่list(range(...))เท่านั้นที่จองทั้งหมด - “
a < b in cหมายถึง(a < b) in c” มันหมายถึงa < b and b in c—inเข้าร่วม chain การเปรียบเทียบของ Python เหมือน< - “
lambdaใน loop จับค่าปัจจุบันของตัวแปร loop” มันจับ ชื่อ ที่ถูก resolve ตอน lambda ถูกเรียก ไม่ใช่ตอนถูกสร้าง ซึ่งตอนนั้น loop มักจบไปแล้ว - “
for...elseรัน blockelseเมื่อ loop วนศูนย์รอบ” มันรัน blockelseเมื่อ loop จบโดยไม่โดนbreak— รวมถึงตอนวนศูนย์รอบด้วย แต่ก็รวมทุกครั้งที่ไม่มีอะไร break ออกไปด้วย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- loop เรียก
.remove(),.pop(),del, หรือ.append()บน list ตัวเดียวกับที่กำลัง iterate อยู่หรือเปล่า? นี่มักจะเป็น bug เสมอ — ตรวจดูว่าควร iterate เหนือ copy หรือสร้าง list ใหม่แทน lambdaหรือ nested function ที่สร้างใน loop อ้างตัวแปร loop ตรง ๆ แทนที่จะจับมันเป็น default argument หรือเปล่า? นั่นคือ late binding ที่รอทำให้ผู้เรียกทุกคนประหลาดใจ- chained comparison มี
in,is, หรือ operator ผสมกันในแบบที่อาจไม่ได้หมายความตามที่มันดูเหมือนหรือเปล่า? - integer division หรือ modulo บนค่าที่อาจเป็นลบสมมติว่าปัดเข้าหาศูนย์ แทนที่จะเป็นพฤติกรรม floor เข้าหาลบอนันต์ของ Python หรือเปล่า?
breakใน nested loop ไปถึง outer loop จริงหรือเปล่า หรือโค้ดสมมติว่าbreakออกจากทุกอย่างพร้อมกัน?
🐛 หา bug ในโค้ดนี้
def remove_duplicates(items): for item in items: if items.count(item) > 1: items.remove(item) return itemsเฉลย
loop นี้แก้ items — list ตัวเดียวกับที่กำลัง iterate อยู่ — ทุกครั้งที่ลบ duplicate ออก การลบ element เลื่อน element ที่เหลือทุกตัวมาข้างหน้าหนึ่งตำแหน่ง แต่ตัวนับตำแหน่งของ loop ไม่รู้เรื่องนั้นเลย มันจึงข้าม element ถัดไปได้ หรือเยี่ยม element ที่จัดการไปแล้วซ้ำ ผลลัพธ์ขึ้นอยู่กับ input ในแบบที่คาดเดายาก และ duplicate บางตัวอาจรอด แก้ด้วยการ iterate เหนือ copy ในขณะที่ยังแก้ (หรือสร้าง) list จริง: for item in items.copy(): ... หรือดีกว่านั้นคือสร้าง list ที่ไม่มี duplicate ใหม่โดยไม่แก้ input เลย เช่น list(dict.fromkeys(items)) เพื่อลบ duplicate โดยรักษาลำดับไว้