# Decorator ใน Python: คืออะไร ทำงานอย่างไร และเขียนเองอย่างไร

URL: https://skillaria.top/th/guides/python/python-decorators

Decorator ใน Python คือฟังก์ชันที่รับฟังก์ชันหนึ่งเข้าไป และคืนฟังก์ชันใหม่ ซึ่งมักเป็นฟังก์ชันห่อหุ้ม (wrapper) ที่เพิ่มพฤติกรรมก่อนหรือหลังการเรียก เช่น การบันทึกล็อก การจับเวลา การแคช หรือการตรวจสิทธิ์ การเขียน @decorator เหนือ def เป็นทางลัดของ func = decorator(func) และเพื่อให้ wrapper คงชื่อและ docstring ของฟังก์ชันเดิม ให้ใช้ functools.wraps

Decorator ใน Python คือฟังก์ชันที่รับฟังก์ชันหนึ่งเข้าไป และคืนฟังก์ชันใหม่ออกมา ซึ่งส่วนใหญ่เป็นฟังก์ชันห่อหุ้ม (wrapper) ที่ครอบฟังก์ชันเดิมไว้ วิธีนี้ทำให้เพิ่มพฤติกรรม เช่น การบันทึกล็อก การแคช หรือการลองใหม่เมื่อเกิดข้อผิดพลาด ได้โดยไม่ต้องแก้โค้ดของฟังก์ชันนั้น decorator เขียนด้วยการใส่ `@ชื่อ` เหนือการประกาศฟังก์ชัน และพบได้ทั่วไปใน Flask, FastAPI, pytest และไลบรารีมาตรฐาน

## decorator คืออะไร อธิบายอย่างง่าย

ลองนึกถึงของขวัญที่ห่อด้วยกระดาษห่อ ตัวของขวัญ (ฟังก์ชัน) ยังเหมือนเดิม แต่มีชั้นห่อด้านนอกที่ทำงานก่อนเป็นอันดับแรก decorator ก็เป็นการห่อแบบนี้ เมื่อมีการเรียกฟังก์ชัน โค้ดของ wrapper จะทำงานก่อน จากนั้นเรียกฟังก์ชันเดิม และสามารถจัดการกับผลลัพธ์ได้อีกครั้ง

สิ่งนี้เป็นไปได้เพราะในภาษา Python ฟังก์ชันคือออบเจกต์ธรรมดา สามารถส่งเป็นอาร์กิวเมนต์ให้ฟังก์ชันอื่น คืนค่าจากฟังก์ชัน และประกาศไว้ภายในฟังก์ชันได้ ทั้งสามคุณสมบัตินี้คือรากฐานของ decorator

## decorator ทำงานอย่างไร: @ คือไวยากรณ์ทางลัด

ลองดู decorator ที่พิมพ์ข้อความก่อนและหลังการเรียกฟังก์ชัน:

```
def log_calls(func):
    def wrapper():
        print(f"กำลังเรียก {func.__name__}")
        result = func()
        print(f"{func.__name__} ทำงานเสร็จ")
        return result
    return wrapper

@log_calls
def hello():
    print("สวัสดี!")

hello()
```

```
กำลังเรียก hello
สวัสดี!
hello ทำงานเสร็จ
```

การเขียน `@log_calls` เหนือ `def` เทียบเท่ากับการเขียนคำสั่งนี้หลังการประกาศ:

```
def hello():
    print("สวัสดี!")

hello = log_calls(hello)
```

ตอนนี้ชื่อ `hello` ชี้ไปที่ `wrapper` ส่วนฟังก์ชันเดิมอยู่ในปิดล้อม (closure) เพราะ `wrapper` จำตัวแปร `func` จากฟังก์ชันภายนอกได้ ประเด็นสำคัญคือ decorator ถูกนำไปใช้เพียงครั้งเดียว ตอนที่คำสั่ง `def` ทำงาน ซึ่งก็คือตอนที่โมดูลถูก import แต่ wrapper จะทำงานทุกครั้งที่มีการเรียก

## closure และ decorator

closure คือฟังก์ชันซ้อนที่จำตัวแปรของฟังก์ชันภายนอกไว้แม้ฟังก์ชันภายนอกจะทำงานจบไปแล้ว หากไม่มี closure decorator ก็จะไม่มี เพราะ `wrapper` ได้เข้าถึง `func` ผ่านกลไกนี้ นอกจากนี้ closure ยังเก็บสถานะได้ เช่น ตัวนับจำนวนการเรียก:

```
def count_calls(func):
    calls = 0
    def wrapper(*args, **kwargs):
        nonlocal calls
        calls += 1
        print(f"{func.__name__}: เรียกครั้งที่ {calls}")
        return func(*args, **kwargs)
    return wrapper
```

## วิธีเขียน decorator ใน Python: เทมเพลตมาตรฐาน

ตัวอย่างแรกมีข้อจำกัดสองข้อ คือ wrapper ไม่รับอาร์กิวเมนต์ และ "กลืน" ชื่อของฟังก์ชันเดิม เทมเพลตที่ใช้เป็นจุดเริ่มต้นของ decorator ทุกตัวได้ดังนี้:

```
import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # โค้ดก่อนเรียกฟังก์ชัน
        result = func(*args, **kwargs)
        # โค้ดหลังเรียกฟังก์ชัน
        return result
    return wrapper
```

กฎสามข้อของเทมเพลต:

1. **`*args, **kwargs`** wrapper รับอาร์กิวเมนต์ได้ทุกแบบและส่งต่อโดยไม่เปลี่ยนแปลง
2. **`return result`** อย่าลืมคืนผลลัพธ์ ไม่เช่นนั้นฟังก์ชันที่ถูกตกแต่งจะคืน `None` เสมอ
3. **`@functools.wraps(func)`** คัดลอกข้อมูลเมตาของฟังก์ชันเดิมไปยัง wrapper

ตัวอย่างของ decorator ที่จับเวลาการทำงาน:

```
import functools
import time

def timer(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__} ใช้เวลา {elapsed:.3f} วินาที")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(range(n))

slow_sum(10_000_000)   # slow_sum ใช้เวลา 0.142 วินาที
```

## functools.wraps ใช้ทำอะไร

หากไม่ใช้ `wraps` ฟังก์ชันที่ถูกตกแต่งจะถูกแทนที่ชื่อและ docstring และอื่น ๆ ทำให้ภายนอกเห็นเป็น `wrapper`:

```
def plain(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """บวกเลขสองตัว"""
    return a + b

print(add.__name__, add.__doc__)   # wrapper None
```

เมื่อใช้ `@functools.wraps(func)` wrapper จะได้ `__name__` `__doc__` `__module__` `__qualname__` และ `__annotations__` ของฟังก์ชันเดิม และเก็บการอ้างอิงถึงต้นฉบับไว้ในแอตทริบิวต์ `__wrapped__` สิ่งนี้สำคัญต่อการดีบัก traceback คำสั่ง `help()` เฟรมเวิร์กที่ลงทะเบียนฟังก์ชันด้วยชื่อ และ `inspect.signature` ที่แสดงลายเซ็นจริง

## decorator ที่มีพารามิเตอร์ (มีอาร์กิวเมนต์)

บางครั้ง decorator ต้องการการตั้งค่า เช่น `@retry(times=3)` หรือ `@app.route("/")` วิธีคือเพิ่มชั้นการซ้อนอีกหนึ่งชั้น ฟังก์ชันชั้นนอกรับพารามิเตอร์และคืน decorator จริงออกมา โครงสร้างแบบนี้เรียกว่าโรงงานของ decorator (decorator factory)

```
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def ping():
    print("ping")

ping()   # ping, ping, ping — สามบรรทัด
```

ลำดับการทำงานคือ `repeat(times=3)` คืน `decorator` ซึ่งถูกนำไปใช้กับ `ping` และคืน `wrapper` กล่าวคือ `@repeat(times=3)` เท่ากับ `ping = repeat(times=3)(ping)`

ตัวอย่างที่นำไปใช้ได้จริงคือการลองใหม่เมื่อเกิดข้อผิดพลาด:

```
import functools
import time

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
    ...
```

## decorator หลายตัวบนฟังก์ชันเดียว

สามารถวาง decorator ซ้อนกันได้ การนำไปใช้จะเรียงจากล่างขึ้นบน คือตัวที่อยู่ใกล้ `def` ที่สุดจะถูกนำไปใช้ก่อน

```
@timer
@log_calls
def report():
    ...

# เท่ากับ:
report = timer(log_calls(report))
```

ตอนเรียกใช้ decorator ชั้นนอก (`timer`) จะทำงานก่อน ดังนั้นมันจะจับเวลารวมทั้งการทำงานของ `log_calls` ด้วย ลำดับมีความสำคัญ เช่น decorator ที่ตรวจสิทธิ์และ decorator ที่แคชผลลัพธ์ จะให้ผลต่างกันเมื่อสลับตำแหน่ง

## decorator มาตรฐานและที่มีมาในตัวของ Python

มี decorator ที่มีประโยชน์หลายตัวอยู่แล้วในภาษาและไลบรารีมาตรฐาน:

| Decorator | หน้าที่ |
| --- | --- |

| `@property` | เปลี่ยนเมธอดให้เป็นแอตทริบิวต์อ่านอย่างเดียว เมื่อใช้คู่กับ `@x.setter` จะกลายเป็นแอตทริบิวต์ที่ควบคุมการกำหนดค่าได้ |

| `@staticmethod` | เมธอดของคลาสที่ไม่มี `self` เป็นฟังก์ชันธรรมดาที่อยู่ในเนมสเปซของคลาส |

| `@classmethod` | เมธอดรับคลาส `cls` แทนอินสแตนซ์ ใช้เขียนตัวสร้างทางเลือก |

| `@functools.cache`, `@functools.lru_cache` | แคชผลลัพธ์ตามอาร์กิวเมนต์ |

| `@functools.wraps` | คัดลอกข้อมูลเมตาของฟังก์ชันไปยัง wrapper |

| `@dataclasses.dataclass` | สร้าง `__init__` `__repr__` `__eq__` ให้คลาส |

| `@contextlib.contextmanager` | เปลี่ยน generator ให้เป็น context manager สำหรับ `with` |

### การแคช: functools.cache และ lru_cache

decorator สำหรับแคชจะจำผลลัพธ์ของอาร์กิวเมนต์แต่ละชุด และเมื่อเรียกซ้ำจะคืนค่าที่เก็บไว้โดยไม่ต้องคำนวณใหม่ ตัวอย่างคลาสสิกคือการคำนวณลำดับฟีโบนัชชีแบบเรียกซ้ำ ซึ่งหากไม่มีแคชจะใช้เวลาเพิ่มขึ้นแบบทวีคูณ:

```
import functools

@functools.cache
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))   # 354224848179261915075
```

`functools.cache` (มีตั้งแต่ Python 3.9) เก็บผลลัพธ์ทั้งหมดโดยไม่จำกัดขนาด ส่วน `@functools.lru_cache(maxsize=128)` เก็บเฉพาะรายการล่าสุดและลบรายการเก่าออก ซึ่งปลอดภัยกว่าสำหรับบริการที่ทำงานต่อเนื่องนาน ๆ ดูสถิติได้ด้วย `fib.cache_info()` และล้างแคชด้วย `fib.cache_clear()` อาร์กิวเมนต์ของฟังก์ชันที่แคชต้องแฮชได้ การส่งลิสต์เข้าไปจะเกิด `TypeError: unhashable type: 'list'` และควรแคชเฉพาะฟังก์ชันบริสุทธิ์ (pure function) คือฟังก์ชันที่ให้ผลเดิมเสมอเมื่ออาร์กิวเมนต์เดิม และไม่มีผลข้างเคียง

### property ทำอะไร

`@property` ทำให้เรียกเมธอดเหมือนแอตทริบิวต์ ไม่ต้องใส่วงเล็บ และควบคุมการกำหนดค่าได้เมื่อจำเป็น:

```
class Account:
    def __init__(self, balance):
        self._balance = balance

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("ยอดคงเหลือต้องไม่ติดลบ")
        self._balance = value

acc = Account(100)
print(acc.balance)   # 100
acc.balance = -5     # ValueError
```

### staticmethod และ classmethod

```
class Money:
    def __init__(self, kopecks):
        self.kopecks = kopecks

    @classmethod
    def from_rubles(cls, rubles):
        return cls(round(rubles * 100))

    @staticmethod
    def is_valid(value):
        return value >= 0

m = Money.from_rubles(12.5)
print(m.kopecks, Money.is_valid(-1))   # 1250 False
```

`@classmethod` ทำให้เมธอดทำงานกับตัวคลาสเอง ไม่ใช่อินสแตนซ์ ตัวอย่างนี้ `from_rubles` จะสร้างอ็อบเจกต์ของคลาสย่อยได้อย่างถูกต้องด้วย

## decorator ของคลาส และคลาสที่เป็น decorator

decorator สามารถนำไปใช้กับคลาสได้ โดยรับคลาสเข้าไปและคืนคลาส (เดิมหรือที่ปรับแล้ว) ตัวอย่าง `@dataclass` ก็ทำงานแบบนี้ ตัวอย่างง่าย ๆ คือรีจิสทรีของคลาส:

```
registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class CsvExporter:
    pass

print(registry)   # {'CsvExporter': }
```

ในทางกลับกัน คลาสก็สามารถทำหน้าที่เป็น decorator ได้ โดยออบเจกต์ใดก็ตามที่มีเมธอด `__call__` ใช้ได้ และการเก็บสถานะทำได้สะดวกผ่านแอตทริบิวต์ของอินสแตนซ์:

```
import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0

    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)

@CountCalls
def tick():
    pass

tick(); tick()
print(tick.calls)   # 2
```

## decorator สำหรับฟังก์ชันแบบ async

สำหรับฟังก์ชันที่ประกาศด้วย `async def` wrapper ต้องเป็นคอรูทีนด้วยและต้องรอผลลัพธ์ด้วย `await` ไม่เช่นนั้นจะได้ออบเจกต์คอรูทีนที่ยังไม่ได้ทำงานกลับมา:

```
import functools

def log_async(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        print(f"เริ่ม {func.__name__}")
        result = await func(*args, **kwargs)
        print(f"เสร็จ {func.__name__}")
        return result
    return wrapper
```

## ทำไมต้องใช้ decorator ใน Python

- **ตรรกะที่ใช้ร่วมกัน** เช่น การบันทึกล็อก การวัดค่า และการจับเวลา โดยไม่ต้องคัดลอกโค้ดไปทุกฟังก์ชัน
- **การแคช** ด้วย `@functools.cache` สำหรับการคำนวณที่แพงและเป็นฟังก์ชันบริสุทธิ์
- **การตรวจสอบ** เช่น สิทธิ์การเข้าถึง การตรวจสอบอาร์กิวเมนต์ และการยืนยันตัวตนในตัวจัดการของเว็บ
- **การลงทะเบียน** เช่น เส้นทางใน Flask และ FastAPI (`@app.get("/")`) และ fixture ของ pytest
- **ความน่าเชื่อถือ** เช่น การลองใหม่ การกำหนดเวลาหมดอายุ และการจัดการข้อผิดพลาด

ข้อดีหลักคือการแยกความรับผิดชอบ ฟังก์ชันทำหน้าที่ของตัวเอง ส่วนพฤติกรรมเสริมถูกเพิ่มด้วยบรรทัดเดียว และลบออกได้ง่ายเช่นกัน

## ข้อผิดพลาดที่พบบ่อย

- **wrapper ไม่คืนผลลัพธ์** ฟังก์ชันหลังการตกแต่งจะคืน `None`
- **ไม่มี `*args, **kwargs`** จะเกิด `TypeError` เมื่อเรียกด้วยอาร์กิวเมนต์
- **ลืม `wraps`** ทำให้ `__name__` และ docstring ผิด และ log สับสน
- **ลืมวงเล็บของโรงงาน** เขียน `@repeat` แทน `@repeat(3)` ทำให้ฟังก์ชันไปอยู่ในพารามิเตอร์ `times`
- **โค้ดหนักในตัว decorator** ทำงานตอน import โมดูล ไม่ใช่ตอนเรียกใช้

## สรุป

Decorator ใน Python คือฟังก์ชันระดับสูง (higher-order function) ที่รับฟังก์ชันและคืน wrapper ส่วน `@` เป็นเพียงรูปแบบย่อของการกำหนดค่า เริ่มจากเทมเพลตที่มี `*args, **kwargs` `return result` และ `functools.wraps` หากต้องการการตั้งค่า ให้เพิ่มชั้นของโรงงาน ฟังก์ชัน closure และ annotation ที่ decorator ต้องพึ่งพาถูกสอนแบบลงมือทำในคอร์สเรื่อง Python

## FAQ

**ถอด decorator ออกจากฟังก์ชันได้หรือไม่?**

ถ้า decorator ใช้ functools.wraps ต้นฉบับจะอยู่ที่แอตทริบิวต์ __wrapped__ เรียก func.__wrapped__(...) ได้โดยไม่ผ่าน wrapper มิฉะนั้นต้องลบบรรทัดที่มี @ ออกจากโค้ด

**โค้ดของ decorator ทำงานเมื่อใด?**

ตัวของ decorator ทำงานครั้งเดียวตอนประกาศฟังก์ชัน ซึ่งมักเป็นตอนที่โมดูลถูก import ส่วนโค้ดภายใน wrapper ทำงานทุกครั้งที่เรียกฟังก์ชันที่ถูกตกแต่ง

**decorator ใช้กับเมธอดของคลาสได้หรือไม่?**

ได้ decorator ทั่วไปที่ใช้ *args, **kwargs ทำงานกับเมธอดได้ โดย self จะเป็นสมาชิกตัวแรกของ args

**decorator ต่างจากการสืบทอด (inheritance) อย่างไร?**

การสืบทอดเปลี่ยนพฤติกรรมของคลาสผ่านลำดับชั้น ส่วน decorator ห่อฟังก์ชันหรือคลาสจากภายนอก และสามารถนำมาผสมกันได้หลายตัวโดยไม่ต้องแก้โค้ดเดิม


---
Source: Skillaria.Top (https://skillaria.top)
