Decorators
Function ที่แปลงร่าง function อื่น
หัวข้อที่มีชื่อว่า “Function ที่แปลงร่าง function อื่น”decorator คือ function ที่รับ function เป็น argument และ return function ตัวใหม่ออกมา
เนื่องจาก function เป็น first-class object ใน Python decorator จึงเป็นแค่ function ธรรมดาตัวหนึ่ง ไม่มี syntax พิเศษอะไรเลยจนกว่าเราจะเติม @
def my_decorator(func): def wrapper(*args, **kwargs): print("Before") result = func(*args, **kwargs) print("After") return result return wrapper
def say_hello(): print("Hello!")
say_hello = my_decorator(say_hello) # manual decorationsay_hello()# Before# Hello!# Aftersyntax @
หัวข้อที่มีชื่อว่า “syntax @”syntax @decorator เป็นรูปแบบย่อ จะใช้ decorator โดยอัตโนมัติทันทีหลังจากที่ function ถูกนิยามเสร็จ
สองแบบนี้เทียบเท่ากันทุกประการ:
@my_decoratordef say_hello(): print("Hello!")
# is identical to:def say_hello(): print("Hello!")say_hello = my_decorator(say_hello)รักษา metadata ไว้ด้วย functools.wraps
หัวข้อที่มีชื่อว่า “รักษา metadata ไว้ด้วย functools.wraps”ถ้าไม่ใช้ functools.wraps function ตัว wrapper จะไปแทนที่ __name__, __doc__ และ attribute อื่น ๆ ของ function ต้นฉบับ
เรื่องนี้ทำให้ introspection, logging และเครื่องมือทำเอกสารทำงานพัง
import functools
def my_decorator(func): @functools.wraps(func) # copies __name__, __doc__, __annotations__, etc. def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper
@my_decoratordef add(a: int, b: int) -> int: """Return the sum of a and b.""" return a + b
print(add.__name__) # add (not 'wrapper')print(add.__doc__) # Return the sum of a and b.ให้ใช้ @functools.wraps(func) กับ function wrapper ตัวในเสมอ ในทุก decorator ที่เราเขียน
logging decorator ที่ใช้งานจริง
หัวข้อที่มีชื่อว่า “logging decorator ที่ใช้งานจริง”import functoolsimport time
def timed(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f"{func.__name__} finished in {elapsed:.6f}s") return result return wrapper
@timeddef compute(n: int) -> int: return sum(range(n))Decorator ที่รับ argument
หัวข้อที่มีชื่อว่า “Decorator ที่รับ argument”หากต้องการส่ง argument ให้ decorator ให้เพิ่มการซ้อนอีกหนึ่งชั้น นั่นคือ factory function ที่รับ argument เข้ามาแล้ว return decorator ตัวจริงออกไป
import functools
def retry(max_attempts: int = 3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts + 1): try: return func(*args, **kwargs) except Exception as exc: if attempt == max_attempts: raise print(f"Attempt {attempt} failed: {exc}. Retrying...") return wrapper return decorator
@retry(max_attempts=3)def fetch(url: str) -> str: # would call an HTTP client here return f"Response from {url}"@retry(max_attempts=3) จะเรียก retry(max_attempts=3) ก่อน ซึ่งจะ return decorator ออกมา
จากนั้น Python จึงนำ decorator ไปใช้กับ fetch ดังนั้น call chain ทั้งหมดคือ fetch = retry(3)(fetch)
เดโมที่รันได้แบบเต็ม
หัวข้อที่มีชื่อว่า “เดโมที่รันได้แบบเต็ม”import functools
def log_calls(level: str = "INFO"): """Decorator factory: log calls at the given level.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): arg_str = ", ".join( [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()] ) print(f"[{level}] {func.__name__}({arg_str}) called") result = func(*args, **kwargs) print(f"[{level}] {func.__name__} returned {result!r}") return result return wrapper return decorator
@log_calls(level="DEBUG")def add(a: int, b: int) -> int: """Return the sum of a and b.""" return a + b
@log_calls()def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!"
print("--- add ---")add(3, 4)
print("--- greet ---")greet("Alice")greet("Bob", greeting="Hi")
print("--- metadata preserved ---")print(add.__name__)print(add.__doc__)Loading Python runtime (first run only)…