Typing & Errors
สองระบบ เป้าหมายเดียว: โค้ดที่เชื่อถือได้
หัวข้อที่มีชื่อว่า “สองระบบ เป้าหมายเดียว: โค้ดที่เชื่อถือได้”Python ให้เครื่องมือเสริมกันสองตัวสำหรับเขียนโค้ดที่พังแบบดังแทนที่จะพังแบบเงียบ ๆ:
- Type hints — annotation ที่อธิบายว่าตัวแปร, พารามิเตอร์ หรือค่าที่คืนควรเก็บ type อะไร ถูกตรวจแบบ static ด้วยเครื่องมืออย่าง mypy หรือ pyright ไม่ใช่ตอน runtime
- Exceptions — กลไกส่งสัญญาณที่ขัดจังหวะการทำงานปกติเมื่อมีอะไรที่ไม่คาดคิดเกิดขึ้น ให้จับและกู้คืน หรือส่ง error ขึ้นไปตาม call stack ได้
รวมกันแล้ว type hints และ exceptions ช่วยให้สื่อสาร intent ได้แม่นยำและจัดการความล้มเหลวได้อย่างสง่างาม
Type hints แบบคร่าว ๆ
หัวข้อที่มีชื่อว่า “Type hints แบบคร่าว ๆ”def greet(name: str, count: int = 1) -> str: return (name + "! ") * countname: str, count: int และ -> str คือ type hints Python เพิกเฉยต่อมันตอน runtime — มีไว้แค่สำหรับ static checker และผู้อ่านโค้ดเท่านั้น มันไม่ทำให้ greet("Alice") ช้าลงหรือเร็วขึ้นแต่อย่างใด
Exception model แบบคร่าว ๆ
หัวข้อที่มีชื่อว่า “Exception model แบบคร่าว ๆ”try: result = int("abc")except ValueError as e: print(f"Caught: {e}")finally: print("Always runs")try ครอบโค้ดที่อาจล้มเหลว except จับ exception type ที่ระบุ finally ทำงานเสมอไม่ว่าจะมี exception เกิดขึ้นหรือไม่
สองระบบนี้เชื่อมกันอย่างไร
หัวข้อที่มีชื่อว่า “สองระบบนี้เชื่อมกันอย่างไร”Type hints กับ exceptions ไม่ได้แยกกันแบบสุดขั้ว — พวกมันทำงานคนละ phase:
| Phase | เครื่องมือ |
|---|---|
| Static analysis (ก่อนรัน) | Type hints + mypy/pyright |
| Runtime failure (ระหว่างรัน) | Exceptions |
function signature บอก caller ว่าคาดหวัง type อะไร exceptions บอก caller ว่าอะไรผิดพลาดได้ตอน runtime
ไม่มีอันไหนแทนที่อีกอันได้: mypy --strict ที่ผ่านสะอาดไม่ได้แปลว่าโปรแกรม crash ไม่ได้ และโปรแกรมที่มี
try/except ระมัดระวังทุกที่ก็ยังอาจ ship ออกไปพร้อม Any กระจายอยู่ทุก signature โดยไม่มี static guarantee
ใด ๆ เลยก็ได้
ตารางตัดสินใจ: raise อะไร, จับอะไร, ปล่อยอะไรผ่านไป
หัวข้อที่มีชื่อว่า “ตารางตัดสินใจ: raise อะไร, จับอะไร, ปล่อยอะไรผ่านไป”ทุกบทในโมดูลนี้ตอบส่วนหนึ่งของตารางนี้ ใช้มันเป็นแผนที่ระหว่างอ่าน หรือเป็น reference เร็ว ๆ ทีหลัง
| สถานการณ์ | ควรทำอะไร | เพราะอะไร |
|---|---|---|
| Input จากภายนอกโปรแกรมไม่ถูกต้อง | raise ValueError/TypeError (หรือ custom subclass) ไม่ใช่ assert | assert ถูกตัดออกทั้งหมดภายใต้ python -O |
| กู้คืนจาก failure เฉพาะเจาะจงได้จริง | except SpecificError: แล้วดำเนินการต่อ | except แบบกว้างซ่อน bug ที่ไม่ใช่ failure ที่ตั้งใจจะจัดการ |
| กู้คืนไม่ได้ แต่ต้อง cleanup | try/finally หรือ context manager | cleanup ต้องทำงานไม่ว่า exception จะถูก handle ในที่สุดหรือไม่ |
| library call ล้มเหลวแล้วต้องการเพิ่ม context | raise NewError(...) from original | เก็บ chain (__cause__) ไว้ แทนที่จะทิ้ง error เดิม |
| ต้อง log แล้วไปต่อที่ boundary ระดับบนสุด | except Exception: (ไม่ใช่ bare except:), logging.exception(...) | Exception ไม่รวม KeyboardInterrupt/SystemExit; logging.exception เก็บ traceback ไว้ |
| พารามิเตอร์ควรรับแค่รูปร่างเดียวที่ชัดเจน | type hint ที่แม่นยำ (Literal, Protocol, Sequence, …) ไม่ใช่ Any | Any ยกเลิกการตรวจแบบ static ทั้งหมด — ดูบท Type Hints |
| operation หลายตัวที่ไม่เกี่ยวข้องกันล้มเหลวพร้อมกันได้ | except* บน ExceptionGroup | except ธรรมดามองเข้าไปใน group ไม่ได้ |
| ตั้งใจเมิน failure เฉพาะเจาะจงหนึ่งตัว | contextlib.suppress(SpecificError) | ระบุชัดว่าเมินอะไร ต่างจาก except: pass |
Module roadmap
หัวข้อที่มีชื่อว่า “Module roadmap”| Lesson | สิ่งที่จะได้เรียน |
|---|---|
| Type Hints | Annotate ตัวแปร, พารามิเตอร์, ค่าที่คืน; Optional, Union, invariance, Any, Protocol, TypedDict, TypeGuard/TypeIs และจุดที่ mypy กับ pyright เห็นต่างกัน |
| Exceptions | Exception hierarchy, try/except/else/finally, raise เทียบกับ raise e, EAFP เทียบกับ LBYL, except*, การอ่าน traceback |
| Context Managers | with, __enter__/__exit__, ตอนที่ __exit__ กลืน exception, ExitStack, contextlib.suppress |
| Custom Errors | Subclass Exception, ทำไม except X as e: ลบ e, chaining แบบ implicit เทียบกับ explicit, add_note() |
Demo แบบ runnable ภาพรวม
หัวข้อที่มีชื่อว่า “Demo แบบ runnable ภาพรวม”from typing import Optional
def find_item(items: list[str], target: str) -> Optional[int]: """Return the index of target in items, or None if not found.""" for i, item in enumerate(items): if item == target: return i return None
fruits = ["apple", "banana", "cherry"]
# type hints describe intent — no runtime enforcementindex = find_item(fruits, "banana")print(f"Found at index: {index}")
missing = find_item(fruits, "mango")print(f"Not found: {missing}")
# exceptions signal runtime failurestry: value = fruits[10]except IndexError as e: print(f"IndexError: {e}")
# the else clause runs only when no exception was raisedtry: value = fruits[1]except IndexError: print("Out of range")else: print(f"Got: {value}")Loading Python runtime (first run only)…
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”แยก failure mode สองแบบไว้ให้ชัด เพราะที่เหลือของโมดูลนี้จัดการทั้งสองต่างกันมาก Type error คือสิ่งที่
checker จับได้ ก่อน โปรแกรมรันเลยด้วยซ้ำ โดยการอ่าน annotation — ไม่มีต้นทุนตอน runtime และจับอะไรไม่ได้เลยถ้า
ไม่มีการรัน checker จริง Runtime exception คือสิ่งที่ Python เองตรวจจับ ระหว่าง โปรแกรมรัน โดย unwind
call stack จนกว่า handler จะจับได้หรือโปรแกรม exit type hints ที่เป็นแค่ของประดับโดยไม่มีการรัน checker ใน CI
และ try/except block ที่ป้องกันไว้โดยไม่คิดว่า exception ไหน ที่คาดไว้ คือสองวิธีที่พบบ่อยที่สุดที่ทำให้ทั้ง
สองระบบไม่ได้ทำอะไรที่มีประโยชน์เลย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| Type hints + mypy/pyright ใน CI | จับ bug หลายประเภทได้ก่อนโค้ดรัน | ต้องมีคนรัน checker จริงและแก้สิ่งที่มันเจอ |
except clause ที่แม่นยำ | bug นอกเหนือจาก failure ที่คาดไว้ยัง crash ให้เห็นชัด | ต้องเขียนหลาย clause กว่า except กว้าง ๆ ตัวเดียว |
| Custom exception hierarchy | caller จับได้ที่ระดับความเจาะจงที่ต้องการ | ต้องนิยาม class เพิ่มและดูแล |
| Context manager สำหรับ cleanup | cleanup รับประกันไม่ว่า block จะออกอย่างไร | มี ceremony มากกว่าการเปิด/ปิดด้วยมือเล็กน้อย |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “type hints กับ exceptions แก้ปัญหาเดียวกัน” — type hints ป้องกัน bug บางประเภทไม่ให้รันได้เลยตั้งแต่แรก ส่วน exceptions จัดการความล้มเหลวที่มีอยู่จริงเพราะโปรแกรมกำลังรันอยู่ (ไฟล์หาย, user input ผิด, network timeout) — ไม่มี static typing ปริมาณเท่าไหร่ที่จะกำจัดสิ่งเหล่านั้นได้
- “ถ้าโค้ดมี type hints แปลว่า mypy ต้องผ่านอยู่แล้ว” — type hints เป็นแค่ annotation ไม่มีอะไรบังคับให้ใคร
รัน checker กับมันจริง ๆ และ
Anyหรือ# type: ignoreทำให้ไฟล์ที่ “typed ครบ” ไม่บังคับใช้อะไรเลยได้ - “การจับ
Exceptionก็แทบจะเหมือนจับทุกอย่าง” — มันตั้งใจไม่รวมKeyboardInterrupt,SystemExitและGeneratorExit— ดูบท Exceptions ว่าทำไมเส้นแบ่งนี้ถึงมีอยู่ - “function ที่ไม่มี
raiseชัดเจนจะไม่มีวันล้มเหลว” — แทบทุก operation (indexing, attribute access, arithmetic, library call) raise ได้เองทั้งนั้น การไม่มี statementraiseไม่ได้บอกอะไรเกี่ยวกับการไม่มี failure mode เลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โมดูลนี้ทั้งหมดฝึกทักษะเดียว: อ่าน signature และ body ของ function แล้วถามว่า “สิ่งนี้ รับประกัน อะไรจริง ๆ เทียบกับแค่ บอกใบ้?” ก่อนเชื่อโค้ดที่ AI generate ในเรื่องนี้:
- พารามิเตอร์หรือ return type ใช้
Anyซึ่งทำให้ function ทั้งตัวหลุดจากการตรวจแบบ static ไปเลยหรือเปล่า? - มี bare
except:หรือexcept Exception: passที่ไม่ log อะไรเลย ซ่อน failure แบบเงียบ ๆ หรือเปล่า? - โค้ดใช้
assertกับอะไรก็ตามที่ต้อง validate จริงใน production ที่-Oจะเอาออกไปหรือเปล่า? - binding ของ
except ... as e:ใน custom exception ถูกอ่านที่ไหนหลัง block ของมันจบไปแล้วหรือเปล่า? - โค้ดถูกรันผ่าน
mypy --strictและpyrightจริง ๆ หรือ type hints แค่ดูสมเหตุสมผลเฉย ๆ?
🐛 หา bug ในโค้ดนี้
from typing import Any
def get_config(data: dict[str, object], key: str) -> Any: try: return data[key] except: return None
value = get_config({"timeout": "30"}, "timeout")print(value + 5)เฉลย
มีปัญหาสองอย่างที่ผสมกันตรงนี้ อย่างแรก -> Any ทำให้ checker ไม่เคยตรวจสอบเลยว่า get_config คืนอะไรจริง ๆ — caller ไม่ได้รับการป้องกันเลย อย่างที่สอง bare except: ทำให้ key ที่หายไปกลายเป็น None แบบเงียบ ๆ แยกไม่ออกจาก error จริง ค่า config "timeout": "30" เป็น str ไม่ใช่ int — get_config คืนมันกลับมาโดยไม่เปลี่ยนแปลง และ value + 5 raise TypeError: can only concatenate str (not "int") to str ห่างไกลจากจุดที่ความผิดพลาดจริงเกิดขึ้น (การเก็บ string แทนตัวเลข หรือไม่เคย validate config เลย) ทางแก้แตะทั้งสองบท: type get_config ให้แม่นยำแทนที่จะใช้ Any และจับ KeyError เฉพาะเจาะจงแทนที่จะจับทุกอย่าง