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

Typing & Errors

Python ให้เครื่องมือเสริมกันสองตัวสำหรับเขียนโค้ดที่พังแบบดังแทนที่จะพังแบบเงียบ ๆ:

  1. Type hints — annotation ที่อธิบายว่าตัวแปร, พารามิเตอร์ หรือค่าที่คืนควรเก็บ type อะไร ถูกตรวจแบบ static ด้วยเครื่องมืออย่าง mypy หรือ pyright ไม่ใช่ตอน runtime
  2. Exceptions — กลไกส่งสัญญาณที่ขัดจังหวะการทำงานปกติเมื่อมีอะไรที่ไม่คาดคิดเกิดขึ้น ให้จับและกู้คืน หรือส่ง error ขึ้นไปตาม call stack ได้

รวมกันแล้ว type hints และ exceptions ช่วยให้สื่อสาร intent ได้แม่นยำและจัดการความล้มเหลวได้อย่างสง่างาม

def greet(name: str, count: int = 1) -> str:
return (name + "! ") * count

name: str, count: int และ -> str คือ type hints Python เพิกเฉยต่อมันตอน runtime — มีไว้แค่สำหรับ static checker และผู้อ่านโค้ดเท่านั้น มันไม่ทำให้ greet("Alice") ช้าลงหรือเร็วขึ้นแต่อย่างใด

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) ไม่ใช่ assertassert ถูกตัดออกทั้งหมดภายใต้ python -O
กู้คืนจาก failure เฉพาะเจาะจงได้จริงexcept SpecificError: แล้วดำเนินการต่อexcept แบบกว้างซ่อน bug ที่ไม่ใช่ failure ที่ตั้งใจจะจัดการ
กู้คืนไม่ได้ แต่ต้อง cleanuptry/finally หรือ context managercleanup ต้องทำงานไม่ว่า exception จะถูก handle ในที่สุดหรือไม่
library call ล้มเหลวแล้วต้องการเพิ่ม contextraise 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, …) ไม่ใช่ AnyAny ยกเลิกการตรวจแบบ static ทั้งหมด — ดูบท Type Hints
operation หลายตัวที่ไม่เกี่ยวข้องกันล้มเหลวพร้อมกันได้except* บน ExceptionGroupexcept ธรรมดามองเข้าไปใน group ไม่ได้
ตั้งใจเมิน failure เฉพาะเจาะจงหนึ่งตัวcontextlib.suppress(SpecificError)ระบุชัดว่าเมินอะไร ต่างจาก except: pass
Lessonสิ่งที่จะได้เรียน
Type HintsAnnotate ตัวแปร, พารามิเตอร์, ค่าที่คืน; Optional, Union, invariance, Any, Protocol, TypedDict, TypeGuard/TypeIs และจุดที่ mypy กับ pyright เห็นต่างกัน
ExceptionsException hierarchy, try/except/else/finally, raise เทียบกับ raise e, EAFP เทียบกับ LBYL, except*, การอ่าน traceback
Context Managerswith, __enter__/__exit__, ตอนที่ __exit__ กลืน exception, ExitStack, contextlib.suppress
Custom ErrorsSubclass Exception, ทำไม except X as e: ลบ e, chaining แบบ implicit เทียบกับ explicit, add_note()
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 enforcement
index = find_item(fruits, "banana")
print(f"Found at index: {index}")
missing = find_item(fruits, "mango")
print(f"Not found: {missing}")
# exceptions signal runtime failures
try:
value = fruits[10]
except IndexError as e:
print(f"IndexError: {e}")
# the else clause runs only when no exception was raised
try:
value = fruits[1]
except IndexError:
print("Out of range")
else:
print(f"Got: {value}")

แยก 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 hierarchycaller จับได้ที่ระดับความเจาะจงที่ต้องการต้องนิยาม class เพิ่มและดูแล
Context manager สำหรับ cleanupcleanup รับประกันไม่ว่า 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 ได้เองทั้งนั้น การไม่มี statement raise ไม่ได้บอกอะไรเกี่ยวกับการไม่มี failure mode เลย

โมดูลนี้ทั้งหมดฝึกทักษะเดียว: อ่าน 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 เฉพาะเจาะจงแทนที่จะจับทุกอย่าง

Python ทำอะไรกับ type hints ตอน runtime?
block ไหนใน try statement ทำงานเสมอ ไม่ว่าจะมี exception เกิดขึ้นหรือไม่?
block else ของ try statement ทำงานเมื่อไหร่?
เครื่องมือไหนตรวจ Python type hints แบบ static?
โค้ดนี้เกิดอะไรขึ้นจริง ๆ ตอนรัน และทำไม signature สไตล์ -> Any ถึงเป็นส่วนหนึ่งของปัญหา?
def get_config(data: dict, key: str):
    try:
        return data[key]
    except:
        return None

print(get_config({}, "port") + 1)
function หนึ่ง annotate ด้วย type ที่แม่นยำครบทุกที่ แต่ทีมไม่เคยรัน mypy หรือ pyright ใน CI เลย type hints เพียงอย่างเดียวรับประกันอะไรได้จริง?
โค้ดนี้พิมพ์อะไร?
def cleanup():
    try:
        raise ValueError("boom")
    finally:
        return "done"

print(cleanup())