On this page
Decorator
เพิ่มความสามารถให้ฟังก์ชัน (เช่น จับเวลา, log, cache) โดยไม่ต้องแก้ตัวฟังก์ชันเดิม
decorator คือฟังก์ชันที่ "ห่อ" ฟังก์ชันอื่นเพื่อเพิ่มพฤติกรรมก่อน/หลังการทำงาน โดยไม่แตะโค้ดเดิม คุณเห็นมันทุกวันในงานจริง เช่น @app.route ของ Flask, @pytest.fixture, @property — หัวข้อนี้ต่อยอดตรงจาก closure
decorator ที่ง่ายที่สุด
decorator รับฟังก์ชันเข้าไป สร้างฟังก์ชันใหม่ที่ห่อของเดิม แล้วคืนออกมา — สังเกตว่าใช้ closure จับ func ไว้
def my_decorator(func):
def wrapper():
print("ก่อนเรียกฟังก์ชัน")
func()
print("หลังเรียกฟังก์ชัน")
return wrapper
def say_hi():
print("สวัสดี")
say_hi = my_decorator(say_hi) # ห่อด้วย decorator
say_hi()
# ก่อนเรียกฟังก์ชัน
# สวัสดี
# หลังเรียกฟังก์ชันsyntax @ — ทางลัดที่ใช้จริง
การเขียน @my_decorator เหนือ def คือทางลัดของ say_hi = my_decorator(say_hi) เป๊ะ ๆ — ผลเหมือนกันแต่อ่านสะอาดกว่า
@my_decorator # = say_hi = my_decorator(say_hi)
def say_hi():
print("สวัสดี")
say_hi()decorator เหมือนกระดาษห่อของขวัญที่เพิ่มความสามารถให้กล่อง โดยไม่ต้องแกะกล่องไปแก้ของข้างใน — ฟังก์ชันเดิมไม่ถูกแตะ แต่ได้พฤติกรรมเพิ่ม
รองรับฟังก์ชันที่มี argument ด้วย *args/**kwargs
wrapper ต้องรับและส่งต่อ argument ทั้งหมด ไม่งั้นห่อได้แค่ฟังก์ชันที่ไม่มี argument — ใช้ *args, **kwargs จากหัวข้อก่อนหน้า
import functools
def timer(func):
@functools.wraps(func) # รักษาชื่อ/docstring ของ func เดิม
def wrapper(*args, **kwargs):
import time
start = time.perf_counter()
result = func(*args, **kwargs) # ส่งต่อ argument ทั้งหมด
elapsed = time.perf_counter() - start
print(f"{func.__name__} ใช้เวลา {elapsed:.4f} วินาที")
return result # อย่าลืมคืนค่า!
return wrapper
@timer
def slow_add(a, b):
import time
time.sleep(0.1)
return a + b
print(slow_add(2, 3)) # slow_add ใช้เวลา 0.10.. วินาที / แล้ว 5ถ้าไม่ใส่ @functools.wraps(func) ฟังก์ชันที่ถูกห่อจะเสียชื่อจริง (slow_add.__name__ กลายเป็น 'wrapper') และ docstring หาย ทำให้ debug และเครื่องมืออื่นสับสน ใส่ไว้เสมอเป็นนิสัย
decorator ที่รับ argument
ถ้าอยากให้ decorator ปรับแต่งได้ เช่น @retry(times=3) ต้องเพิ่มอีกชั้น — ฟังก์ชันนอกสุดรับ argument แล้วคืน decorator จริง
import functools
def retry(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"ครั้งที่ {attempt} ล้มเหลว: {e}")
raise RuntimeError(f"ล้มเหลวครบ {times} ครั้ง")
return wrapper
return decorator
@retry(times=3)
def risky():
raise ValueError("พัง")
# risky() -> ลอง 3 ครั้งแล้วค่อย raisedecorator ที่เจอในงานจริง
- @app.route("/") — Flask ผูก URL กับฟังก์ชัน
- @pytest.fixture — เตรียมข้อมูลให้เทสต์ (เจอในบท Testing)
- @property — ทำ method ให้เรียกเหมือน attribute
- @functools.lru_cache — cache ผลลัพธ์ (เจอในบท Performance & DP)
สรุปหัวข้อนี้
- decorator = ฟังก์ชันที่ห่อฟังก์ชันอื่นเพื่อเพิ่มพฤติกรรม
- @decorator คือทางลัดของ func = decorator(func)
- wrapper ต้องใช้ *args/**kwargs ส่งต่อ argument และคืนค่ากลับ
- ใส่ @functools.wraps(func) เสมอ; decorator รับ argument = 3 ชั้น
1) เขียน decorator @timer วัดเวลาฟังก์ชัน (ลองกับฟังก์ชันที่มี argument) 2) เขียน @log_call ที่ print ชื่อฟังก์ชันและ argument ทุกครั้งที่ถูกเรียก 3) เขียน decorator @memoize ที่ cache ผลลัพธ์ใน dict 4) อธิบายว่าทำไมต้องใส่ functools.wraps