How to read Python you did not write
ทุกโมดูลก่อนหน้าในคอร์สนี้สอนให้คุณ เขียน Python โมดูลนี้สอนอีกครึ่งหนึ่งของงาน: การอ่านโค้ด Python ที่คนอื่น — หรือสิ่งอื่น — เขียนไว้ แล้วตัดสินว่ามันถูกต้องหรือไม่ AI assistant จะส่งโค้ดที่รันได้ พิมพ์ output ที่ดูสมเหตุสมผล และดูเหมือนตัวอย่างที่ idiomatic ที่คุณเรียนมาทุกประการ การรันได้โดยไม่มี error ไม่เหมือนกับการถูกต้อง คำถามสี่ข้อด้านล่างคือลำดับที่ผู้รีวิว Python ที่มีประสบการณ์ใช้อ่านโค้ดจริง ๆ และทุกบทถัดไปในโมดูลนี้จะเจาะลึกข้อใดข้อหนึ่ง
ตาม mutable data flow
หัวข้อที่มีชื่อว่า “ตาม mutable data flow”Python ส่ง argument ทุกตัวแบบ pass-by-object-reference และ object ส่วนใหญ่ที่คุณส่งไปมา — list, dict, set และ instance ของ class ที่ไม่ใช่ frozen=True — เป็น mutable ก่อนจะอ่านว่า function return อะไร ให้อ่านก่อนว่ามัน แตะ อะไรบ้าง:
- function นี้ mutate argument ในที่เดิม, return object ใหม่, หรือทั้งสองอย่าง?
- ถ้ามัน return object เดิมที่ได้รับมา (
return the_list) ผู้เรียกคาดหวัง list ใหม่ หรือคาดหวังว่าการ mutate กับค่าที่ return จะเป็นสิ่งเดียวกัน? - default argument (
def f(items=[])) เป็น mutable object ที่ถูกสร้างครั้งเดียวตอน definition แล้วถูกแชร์ข้ามทุก call ที่ไม่ได้ส่งของตัวเองมาหรือเปล่า? - class attribute ที่ประกาศระดับ class (ไม่ใช่ใน
__init__) ถูกแชร์ข้ามทุก instance เหมือนที่ module-level variable ถูกแชร์ข้ามทุกผู้เรียกหรือเปล่า?
ทั้งหมดนี้ไม่ต้องรันโค้ดเลย ผู้รีวิวที่อ่าน def restock(inventory: dict[str, int], item: str, qty: int) -> dict[str, int]: แล้วเห็น body ทำ inventory[item] = ... ก่อน return inventory ก็รู้ทันทีว่า: มัน mutate dict ของผู้เรียก และ คืน object เดิมกลับมา — สิ่งต่อไปที่ต้องเช็คคือผู้เรียกทุกตัวคาดหวังแบบนั้นหรือเปล่า
ตาม exception path
หัวข้อที่มีชื่อว่า “ตาม exception path”try/except ของ Python เป็น opt-in ไม่ได้ถูกบังคับโดย compiler แบบที่ error return ของ Go หรือ Rust เป็น — ไม่มีอะไรบังคับให้ผู้เรียกสังเกตว่า function สามารถ raise ได้ การอ่าน function ให้ดีคือการตาม path ความล้มเหลวทุกทาง ไม่ใช่แค่ happy-path return:
- function raise อะไร ภายใต้เงื่อนไขไหน? มีเอกสารระบุไว้ที่ไหน หรือรู้ได้จากการอ่าน body เท่านั้น?
- มี
except Exception(หรือ bareexcept:) ที่กว้างเกินไปอยู่ระหว่างจุด raise กับจุดที่ควรจะถูก handle หรือเปล่า — และมันกลืน failure mode มากกว่าตัวเดียวที่ตั้งใจจะจับหรือเปล่า? - มี
assertอยู่ในจุดที่ควรเป็น input validation จริงหรือเปล่า?assertหายไปทั้งหมดภายใต้python -O - ใน async code มี
except Exception(หรือกว้างกว่านั้นคือexcept BaseException) คั่นอยู่ระหว่าง task กับการ.cancel()ของมัน — กลืน cancellation ไปเองหรือเปล่า?
บทที่รวบรวมว่าโค้ดที่ AI generate ทำผิดตรงนี้อย่างไรบ้างคือ The AI Bug Catalog — หน้านี้ถือว่าคุณรู้ try/except/finally จากบท Exceptions แล้ว และจะฝึกให้คุณจับจุดที่ละเมิดกฎเหล่านั้นแทน
ตาม lifetime ของ task และ resource
หัวข้อที่มีชื่อว่า “ตาม lifetime ของ task และ resource”อะไรก็ตามที่มี .close(), with block หรือการเรียก create_task() มี lifetime ที่ Python ไม่ได้ track ให้อัตโนมัติ สำหรับ resource หรือ task ที่ถูกสร้างขึ้นทุกตัว ให้ถามว่า:
- ใครมีหน้าที่ปิดมัน — function ที่เปิดมัน หรืออะไรก็ตามที่มันถูกส่งต่อไปให้?
- การ release ถูกรับประกันในทุก path หรือเปล่า รวมถึง early
returnหรือ exception ที่ raise — ปกติผ่านwithไม่ใช่.close()เปล่า ๆ ท้าย function? - สำหรับการเรียก
asyncio.create_task(...): return value (Taskobject) ถูกเก็บไว้ที่ไหนหรือเปล่า? task ที่ไม่มีใครอ้างอิงถึงสามารถถูก garbage-collect ก่อนที่มันจะทำงานเสร็จได้ ทำให้งานที่มันกำลังทำหายไปแบบเงียบ ๆ - มีอะไร
awaittask นั้นบ้างไหม เพื่อให้ exception ที่ raise ข้างในมันโผล่ออกมาจริง ๆ แทนที่จะหายไปเฉย ๆ?
อ่าน type hint แบบไม่เชื่อ
หัวข้อที่มีชื่อว่า “อ่าน type hint แบบไม่เชื่อ”type hint คือคำกล่าวอ้าง ไม่ใช่การรับประกัน ไม่มีอะไรเช็คมันตอน runtime เว้นแต่คุณจะเพิ่ม validation ของตัวเอง:
- body ของ function ตรงกับสิ่งที่ signature สัญญาไว้จริงหรือเปล่า — หรือมันรับ
Noneแบบเงียบ ๆ ในจุดที่ hint บอกว่าเป็นstrเพราะไม่มีอะไรบังคับใน call ที่โค้ดถูก test ด้วย? - มีการใช้
Anyในจุดที่มี type เฉพาะเจาะจงกว่าให้ใช้ได้หรือเปล่า?Anyไม่ได้แปลว่า “ไม่เช็คตรงนี้” เท่านั้น มันปิดการเช็คของทุกอย่างที่ต่อจากมันด้วย - type hint ถูกเช็คด้วยเครื่องมือจริง (
mypy --strict,pyright) แล้วหรือยัง — หรือแค่ ดูเหมือน สมเหตุสมผลเพราะมัน compile ผ่าน (Python ไม่มีแนวคิด hint ที่ “compile ไม่ผ่าน” อยู่แล้ว)?
def restock(inventory: dict[str, int], item: str, qty: int) -> dict[str, int]: # Reading order in practice: (1) this function MUTATES the dict you pass # in -- it does not copy it -- so every caller's reference to that same # dict sees the change too. (2) It can raise ValueError; the return value # on the happy path is the SAME object as the input, which is easy to # miss if you only skim the final return statement. if qty < 0: raise ValueError(f"qty must be >= 0, got {qty}") inventory[item] = inventory.get(item, 0) + qty return inventory
warehouse = {"widgets": 10}same_dict = restock(warehouse, "widgets", 5)print("warehouse after restock:", warehouse)print("same object returned:", same_dict is warehouse)
try: restock(warehouse, "widgets", -3)except ValueError as e: print(f"restock rejected: {e}")print("warehouse unchanged after the rejected call:", warehouse)Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TD A["Read the code"] --> B["Where does mutable state flow -- mutated in place, or copied?"] B --> C["What can raise here, and who actually catches it?"] C --> D["What owns each task/resource, and who releases it?"] D --> E["Do the type hints hold at runtime, or are they just claims?"] E --> F["Only now: judge correctness"]
Reading order for a function you did not write:
1. Mutable data flow → mutated in place? returned as a new object? both? shared class/default state?2. Exception path → what can raise, is it documented, is it swallowed by something too broad?3. Task/resource life → who closes it, is release guaranteed on every path, is a task ever awaited?4. Type hints → checked by mypy/pyright, or just plausible-looking prose?
Skipping straight to "it ran and printed something reasonable" answers none of these.ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| ตาม mutable data flow ก่อน | จับ bug เรื่อง aliasing และ shared default ได้ก่อนที่จะเชื่อ return value ใด ๆ | ต้องรู้ว่า builtin ตัวไหน mutable และการส่ง argument แบบไหนแชร์ state จริง ๆ |
| ตาม exception path ทั้งหมด | เจอ validation ที่ถูกกลืน, scope ผิด หรือใช้ assert แทนที่โค้ด AI ชอบซ่อนไว้ | ช้ากว่าการอ่านแค่ happy-path return |
| ตาม lifetime ของ task/resource อย่างชัดเจน | เจอ file handle ที่รั่วและ background task ที่หายไปก่อนที่มันจะไป page ใครตอนตี 3 | ต้องเก็บ state ในหัวมากขึ้นตอนอ่าน async code |
| มอง type hint เป็นคำกล่าวอ้าง ไม่ใช่การรับประกัน | เจอช่องว่างระหว่างสิ่งที่ signature สัญญากับสิ่งที่ body ทำจริง | ต้องรัน mypy/pyright เอง — hint อย่างเดียวพิสูจน์อะไรไม่ได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “มันรันได้และพิมพ์อะไรที่ดูสมเหตุสมผล แสดงว่ามันถูกต้อง” ทุก pattern ใน catalog ของโมดูลนี้รันได้และให้ output ที่ดูสมเหตุสมผลบน happy path — นั่นคือสิ่งที่ทำให้มันอันตรายพอดี
- “อ่าน happy-path
returnก็คืออ่าน function แล้ว”exceptclause,assertและโค้ดระหว่างawaitกับบรรทัดถัดไป มักเป็นจุดที่โค้ดที่ AI generate ซ่อนความผิดพลาดไว้ - “ถ้าคนเขียนก็ไม่เป็นไร ต้องระวังเฉพาะโค้ด AI” ทุก pattern ใน catalog ของโมดูลนี้ปรากฏในโค้ด Python ที่คนเขียนด้วยเช่นกัน — AI แค่ผลิตมันในปริมาณที่มากกว่าและดูน่าเชื่อถือกว่ามาก
- “type hint แปลว่าค่านั้นรับประกันว่าเป็น type นั้นแน่นอน” hint ไม่ถูกเช็คตอน runtime เว้นแต่มีอะไรสักอย่าง (validator, checker ใน CI) บังคับใช้จริง ๆ input ที่ไม่น่าเชื่อถือสามารถละเมิด hint ใดก็ได้แบบเงียบ ๆ