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

เจาะลึกเรื่อง Function

ใน Python นั้น function คือค่า (value) ตัวหนึ่ง ไม่ใช่ syntax พิเศษ และไม่ใช่ของชั้นรอง เราสามารถกำหนด function ให้กับตัวแปร เก็บไว้ใน list ส่งเป็น argument หรือ return ออกมาจากอีก function หนึ่งได้ คุณสมบัตินี้เรียกว่าสถานะ first-class ที่เป็นรากฐานของทุก pattern ขั้นสูงในโมดูลนี้

def greet(name: str) -> str:
return f"Hello, {name}!"
# Assign to a variable
say_hi = greet
# Pass as an argument
def 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 & Kwargsparameter แบบ positional, keyword, variadic และ keyword-only
Closuresnested function, free variable และ nonlocal
Decoratorsfunction ที่ห่อ function อื่น; functools.wraps
Generatorsyield, 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 list
ops = [double, triple, make_multiplier(10)]
for op in ops:
print(op(7))

เพราะ 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 chain
SHAPE_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 functions
print(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)

SHAPE_AREA.get(shape) คืนค่า None เมื่อ shape ไม่เคยถูกลงทะเบียนไว้ ซึ่ง compute_area แปลงเป็น ValueError ที่ชัดเจน — เทียบเท่ากับ else สุดท้ายของ if/elif chain แต่ในรูปแบบ dict dispatch callable(x) คือการเช็กทั่วไปว่า “ใส่ () ตามหลังสิ่งนี้ได้ไหม”: มันเป็น True สำหรับ function ธรรมดา แต่เป็น False สำหรับ dict — แม้ว่า value ทุกตัวใน dict นั้นจะเป็น function ก็ตาม

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)

count_down(50) ต้องการแค่ 50 การเรียกซ้อนกัน ต่ำกว่า limit ปกติที่ 1000 มาก จึง return ได้ปกติ ส่วน infinite_recursion ไม่มี base case เลย — ทุกการเรียกก็เรียกซ้ำอีกครั้ง — มันจึงชนกับ recursion limit เสมอ และ Python จะ raise RecursionError แทนที่จะใช้หน่วยความจำจนหมดเครื่อง

function ก็แค่ object ตัวหนึ่งที่มีความสามารถพิเศษเพิ่มมาหนึ่งอย่าง: ใส่ () ต่อท้ายแล้วมันจะรัน ทุกอย่างที่โมดูลนี้จะสอนต่อจากนี้ — closure, decorator, generator, *args/**kwargs — ล้วนสร้างขึ้นบนความจริงข้อเดียวนี้ dict ที่ map string ไปหา function (เช่น {"circle": area_circle}) ทำงานได้เพราะ function เป็นค่าธรรมดาที่วางไว้ตรงไหนที่ค่าหนึ่งวางได้ก็ได้ทั้งนั้น callable(obj) ถามว่า obj รองรับ () หรือเปล่า สำหรับ object ใด ๆ ก็ได้ ไม่ใช่แค่สิ่งที่นิยามด้วย def

สิ่งที่ได้ประโยชน์ต้นทุน
dict dispatch แทน if/elifcase ใหม่คือ 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/elif chain จะตกไปถึง else สุดท้าย เพียงแต่รายงานออกมาเป็น exception แทนการ fallthrough แบบเงียบ ๆ
  • “callable(x) บอกว่า x เป็น function” — ความจริง: มันบอกแค่ว่า x รองรับการถูกเรียกด้วย () class, bound method และ instance ใด ๆ ที่ class ของมันนิยาม __call__ ไว้ก็ callable ได้เหมือนกันโดยไม่ต้องเป็น function
  • 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 ครั้งเดียว

คำว่า 'first-class function' ใน Python หมายความว่าอย่างไร?
Built-in function ตัวใดที่รับ function และ iterable เป็น argument?
หลังจากเขียน f = greet แล้ว ข้อใดถูกต้อง?
โค้ดนี้พิมพ์อะไร? ops = {"add": lambda a, b: a + b, "sub": lambda a, b: a - b} def calc(op, a, b): return ops[op](a, b) print(calc("add", 2, 3)) print(calc("mul", 2, 3))
โค้ดนี้จะเกิดอะไรขึ้นเมื่อรัน? def countdown(n): return countdown(n - 1) countdown(0)
callable(5) จะคืนค่าอะไร และเพราะอะไร?