Notes & software courses · Free to learn
Aph's Blog
On this page

Decorator

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

เพิ่มความสามารถให้ฟังก์ชัน (เช่น จับเวลา, log, cache) โดยไม่ต้องแก้ตัวฟังก์ชันเดิม

decorator คือฟังก์ชันที่ "ห่อ" ฟังก์ชันอื่นเพื่อเพิ่มพฤติกรรมก่อน/หลังการทำงาน โดยไม่แตะโค้ดเดิม คุณเห็นมันทุกวันในงานจริง เช่น @app.route ของ Flask, @pytest.fixture, @property — หัวข้อนี้ต่อยอดตรงจาก closure

decorator ที่ง่ายที่สุด

decorator รับฟังก์ชันเข้าไป สร้างฟังก์ชันใหม่ที่ห่อของเดิม แล้วคืนออกมา — สังเกตว่าใช้ closure จับ func ไว้

python
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) เป๊ะ ๆ — ผลเหมือนกันแต่อ่านสะอาดกว่า

python
@my_decorator          # = say_hi = my_decorator(say_hi)
def say_hi():
    print("สวัสดี")

say_hi()
เปรียบเทียบให้เห็นภาพ

decorator เหมือนกระดาษห่อของขวัญที่เพิ่มความสามารถให้กล่อง โดยไม่ต้องแกะกล่องไปแก้ของข้างใน — ฟังก์ชันเดิมไม่ถูกแตะ แต่ได้พฤติกรรมเพิ่ม

รองรับฟังก์ชันที่มี argument ด้วย *args/**kwargs

wrapper ต้องรับและส่งต่อ argument ทั้งหมด ไม่งั้นห่อได้แค่ฟังก์ชันที่ไม่มี argument — ใช้ *args, **kwargs จากหัวข้อก่อนหน้า

python
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

ถ้าไม่ใส่ @functools.wraps(func) ฟังก์ชันที่ถูกห่อจะเสียชื่อจริง (slow_add.__name__ กลายเป็น 'wrapper') และ docstring หาย ทำให้ debug และเครื่องมืออื่นสับสน ใส่ไว้เสมอเป็นนิสัย

decorator ที่รับ argument

ถ้าอยากให้ decorator ปรับแต่งได้ เช่น @retry(times=3) ต้องเพิ่มอีกชั้น — ฟังก์ชันนอกสุดรับ argument แล้วคืน decorator จริง

python
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 ครั้งแล้วค่อย raise

decorator ที่เจอในงานจริง

  • @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