เจาะลึกเรื่อง Function
Function เป็น first-class object
หัวข้อที่มีชื่อว่า “Function เป็น first-class object”ใน Python นั้น function คือค่า (value) ตัวหนึ่ง ไม่ใช่ syntax พิเศษ และไม่ใช่ของชั้นรอง เราสามารถกำหนด function ให้กับตัวแปร เก็บไว้ใน list ส่งเป็น argument หรือ return ออกมาจากอีก function หนึ่งได้ คุณสมบัตินี้เรียกว่าสถานะ first-class ที่เป็นรากฐานของทุก pattern ขั้นสูงในโมดูลนี้
def greet(name: str) -> str: return f"Hello, {name}!"
# Assign to a variablesay_hi = greet
# Pass as an argumentdef apply(fn, value): return fn(value)
result = apply(greet, "Python")print(result) # Hello, Python!เนื่องจาก greet เป็นแค่ object ตัวหนึ่ง say_hi และ greet จึงชี้ไปยัง function ตัวเดียวกัน
การเรียก say_hi("World") จึงเหมือนกับการเรียก greet("World") ทุกประการ
โมดูลนี้ครอบคลุมอะไรบ้าง
หัวข้อที่มีชื่อว่า “โมดูลนี้ครอบคลุมอะไรบ้าง”| บทเรียน | แนวคิดหลัก |
|---|---|
| Args & Kwargs | parameter แบบ positional, keyword, variadic และ keyword-only |
| Closures | nested function, free variable และ nonlocal |
| Decorators | function ที่ห่อ function อื่น; functools.wraps |
| Generators | yield, lazy evaluation และ StopIteration |
แต่ละบทเรียนนั้นจบในตัวเอง และต่อยอดจากรากฐานนี้ นั่นคือ function เป็น object ที่เราจัดการได้อย่างอิสระ
เดโมที่รันได้อันแรก
หัวข้อที่มีชื่อว่า “เดโมที่รันได้อันแรก”โค้ดด้านล่างแสดงสองแนวคิดหลักพร้อมกัน ได้แก่ การส่ง function เป็น argument และการ return function เป็นค่ากลับมา
def make_multiplier(factor: int): def multiply(x: int) -> int: return x * factor return multiply
double = make_multiplier(2)triple = make_multiplier(3)
numbers = [1, 2, 3, 4, 5]doubled = list(map(double, numbers))tripled = list(map(triple, numbers))
print("doubled:", doubled)print("tripled:", tripled)
# Functions are objects — we can store them in a listops = [double, triple, make_multiplier(10)]for op in ops: print(op(7))Loading Python runtime (first run only)…
Dispatch ด้วย data แทนที่จะเป็น if/elif chain
หัวข้อที่มีชื่อว่า “Dispatch ด้วย data แทนที่จะเป็น if/elif chain”เพราะ function เป็นค่าธรรมดา ๆ ตัวหนึ่ง เราจึงเก็บมันเป็น value ของ dict ได้ โดยใช้ key เป็นอะไรก็ได้ที่เลือก behavior — string, enum, type ก็ได้ทั้งนั้น การค้นหา function ใน dict แล้วเรียกมันแทนที่ if/elif chain ยาว ๆ ได้ด้วยการ lookup dict ครั้งเดียว และการเพิ่ม case ใหม่ก็แค่เพิ่ม entry เดียวใน dict แทนที่จะเพิ่ม elif อีกอัน
def area_circle(r): return 3.14159 * r ** 2
def area_square(s): return s ** 2
def area_rectangle(w, h): return w * h
# Dict dispatch instead of an if/elif chainSHAPE_AREA = { "circle": area_circle, "square": area_square, "rectangle": area_rectangle,}
def compute_area(shape, *args): handler = SHAPE_AREA.get(shape) if handler is None: raise ValueError(f"unknown shape: {shape}") return handler(*args)
print(compute_area("circle", 2))print(compute_area("square", 3))print(compute_area("rectangle", 2, 5))
# callable() asks "does this support ()?" for any object, not just def-defined functionsprint(callable(area_circle))print(callable(SHAPE_AREA))print(callable(42))
try: compute_area("triangle", 1, 2)except ValueError as e: print(type(e).__name__ + ":", e)Loading Python runtime (first run only)…
SHAPE_AREA.get(shape) คืนค่า None เมื่อ shape ไม่เคยถูกลงทะเบียนไว้ ซึ่ง compute_area แปลงเป็น ValueError ที่ชัดเจน — เทียบเท่ากับ else สุดท้ายของ if/elif chain แต่ในรูปแบบ dict dispatch callable(x) คือการเช็กทั่วไปว่า “ใส่ () ตามหลังสิ่งนี้ได้ไหม”: มันเป็น True สำหรับ function ธรรมดา แต่เป็น False สำหรับ dict — แม้ว่า value ทุกตัวใน dict นั้นจะเป็น function ก็ตาม
Recursion: มี limit และไม่มี tail-call optimization
หัวข้อที่มีชื่อว่า “Recursion: มี limit และไม่มี tail-call optimization”recursive function คือ function ที่เรียกตัวเองซ้ำ ๆ เพื่อคืบหน้าไปสู่ base case ทุกการเรียกจะรักษา stack frame ของตัวเองไว้จนกว่าจะ return — CPython ไม่ทำ tail-call optimization เลย ดังนั้นสิ่งนี้เป็นจริงแม้ว่าการเรียกซ้ำจะเป็นสิ่งสุดท้ายที่ function ทำก็ตาม sys.getrecursionlimit() บอกว่า Python ยอมให้เรียกซ้อนกันได้กี่ครั้งก่อนจะยอมแพ้ด้วย RecursionError ไม่ใช่การ crash แบบ stack overflow
import sys
def count_down(n): if n <= 0: return "liftoff" return count_down(n - 1) # not tail-call optimized -- Python keeps every frame
print("recursion limit:", sys.getrecursionlimit())print(count_down(50))
def infinite_recursion(n): return infinite_recursion(n + 1)
try: infinite_recursion(0)except RecursionError as e: print(type(e).__name__ + ":", e)Loading Python runtime (first run only)…
count_down(50) ต้องการแค่ 50 การเรียกซ้อนกัน ต่ำกว่า limit ปกติที่ 1000 มาก จึง return ได้ปกติ ส่วน infinite_recursion ไม่มี base case เลย — ทุกการเรียกก็เรียกซ้ำอีกครั้ง — มันจึงชนกับ recursion limit เสมอ และ Python จะ raise RecursionError แทนที่จะใช้หน่วยความจำจนหมดเครื่อง
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”function ก็แค่ object ตัวหนึ่งที่มีความสามารถพิเศษเพิ่มมาหนึ่งอย่าง: ใส่ () ต่อท้ายแล้วมันจะรัน ทุกอย่างที่โมดูลนี้จะสอนต่อจากนี้ — closure, decorator, generator, *args/**kwargs — ล้วนสร้างขึ้นบนความจริงข้อเดียวนี้ dict ที่ map string ไปหา function (เช่น {"circle": area_circle}) ทำงานได้เพราะ function เป็นค่าธรรมดาที่วางไว้ตรงไหนที่ค่าหนึ่งวางได้ก็ได้ทั้งนั้น callable(obj) ถามว่า obj รองรับ () หรือเปล่า สำหรับ object ใด ๆ ก็ได้ ไม่ใช่แค่สิ่งที่นิยามด้วย def
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
dict dispatch แทน if/elif | case ใหม่คือ dict entry เดียว ไม่ใช่อีกหนึ่ง branch; ตัว mapping เองก็เป็น data ที่เราสร้าง ขยาย หรือตรวจสอบตอน runtime ได้ | key ที่ขาดหายจะ raise KeyError (หรือถูกเมินเฉยแบบเงียบ ๆ ถ้าใช้ .get) แทนที่ Python จะบอกว่าเราลืมเขียน elif อันไหน |
| recursion แทน loop | สะท้อนโครงสร้างแบบ recursive ของปัญหาเอง มักสั้นและใกล้เคียงกับนิยามของปัญหามากกว่า | ทุกการเรียกรักษา stack frame ของตัวเองไว้ — ไม่มี tail-call optimization ใน CPython — recursion ที่ลึกจึงกินหน่วยความจำจริง และอาจชนกับ sys.getrecursionlimit() ได้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Python ทำ tail-call optimization แบบภาษาอื่น ๆ” — ความจริง: CPython ไม่ทำ tail-call optimization เลยแม้แต่น้อย recursive function ที่จะรันไม่มีที่สิ้นสุดจะ raise
RecursionErrorในที่สุด ไม่ว่าการเรียกซ้ำจะถูกวางไว้ตรงไหนก็ตาม - “
KeyErrorจาก dispatch dict แปลว่า dict นั้นมีปัญหา” — ความจริง: มันมักแปลว่า input ไม่ตรงกับ case ไหนที่ลงทะเบียนไว้เลย — สถานการณ์เดียวกับที่if/elifchain จะตกไปถึงelseสุดท้าย เพียงแต่รายงานออกมาเป็น exception แทนการ fallthrough แบบเงียบ ๆ - “
callable(x)บอกว่าxเป็น function” — ความจริง: มันบอกแค่ว่าxรองรับการถูกเรียกด้วย()class, bound method และ instance ใด ๆ ที่ class ของมันนิยาม__call__ไว้ก็ callable ได้เหมือนกันโดยไม่ต้องเป็น function
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”- dispatch table แบบ dict มี fallback สำหรับ key ที่ไม่ได้ลงทะเบียนไหม (
.get(key, default)หรือtry/except KeyError) — หรือ key ที่ขาดหายจะทำให้ call chain ทั้งหมดพังด้วย exception ที่ไม่ได้ถูกจับ? - recursive function มี base case อยู่จริงไหม และทุก input ที่เป็นไปได้ไปถึง base case นั้นได้จริงหรือเปล่า — หรือ base case นั้นขึ้นอยู่กับเงื่อนไขที่ argument ไม่มีวันสนองได้เลย?
- มีการใช้ recursion แบบลึกไม่จำกัดในจุดที่ loop แบบเทียบเท่าจะใช้ stack space คงที่เล็ก ๆ ไม่ว่า input จะขนาดไหนไหม?
- มีการใช้
callable(x)ในจุดที่จริง ๆ ตั้งใจจะเช็กแบบisinstance(x, SomeClass)ไหม — สองอย่างนี้ถามคนละคำถามกัน ใช้แทนกันไม่ได้?
🐛 หา bug ในโค้ดนี้
def factorial(n): return n * factorial(n - 1)
print(factorial(5))เฉลย
factorial ไม่มี base case เลย — ทุกการเรียกก็เรียกซ้ำอีกครั้ง รวมถึง factorial(0), factorial(-1), factorial(-2) ไปเรื่อย ๆ ไม่มีที่สิ้นสุด ตรวจสอบแล้วบน Python 3.14: มันไม่มีวันไปถึงคำตอบที่ถูกต้อง มันจะเรียกซ้ำจน call stack เกิน sys.getrecursionlimit() แล้ว Python จะ raise RecursionError: maximum recursion depth exceeded ทางแก้คือใส่ base case ที่หยุด recursion เช่น if n <= 1: return 1 ก่อนบรรทัดที่เรียกซ้ำ — เป็นบั๊กรูปแบบเดียวกับ if/elif chain หรือ dict-dispatch table ที่ไม่มี case รองรับ input บางตัว เพียงแต่ถูกค้นพบผ่าน stack ของการเรียกแทนที่จะเป็นการ lookup ครั้งเดียว