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

Decorators

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 decoration
say_hello()
# Before
# Hello!
# After

syntax @decorator เป็นรูปแบบย่อ จะใช้ decorator โดยอัตโนมัติทันทีหลังจากที่ function ถูกนิยามเสร็จ สองแบบนี้เทียบเท่ากันทุกประการ:

@my_decorator
def say_hello():
print("Hello!")
# is identical to:
def say_hello():
print("Hello!")
say_hello = my_decorator(say_hello)

ถ้าไม่ใช้ 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_decorator
def 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 ที่เราเขียน

import functools
import 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
@timed
def compute(n: int) -> int:
return sum(range(n))

หากต้องการส่ง 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__)
`@my_decorator` ที่อยู่เหนือนิยามของ function ทำอะไร?
ทำไมเราควรใช้ `@functools.wraps(func)` ภายใน decorator?
เมื่อมี `@retry(max_attempts=3)` อยู่เหนือ `def fetch(): ...` ลำดับการเรียกเป็นอย่างไร?
หาก decorator ถูกซ้อนกัน `@a` แล้ว `@b` แล้ว `@c` บน `def f()` ตัวใดถูกใช้ก่อน?