دکوریتورها در پایتون، قدمبهقدم
دکوریتورها جادویی به نظر میرسند، اما فقط توابعی هستند که توابع دیگر را میپوشانند. دکوریتورهای زمانسنجی و تلاش مجدد را از صفر بسازید.
در این صفحه
احتمالاً کدی شبیه این را در Flask، FastAPI یا Django دیدهاید:
@app.route("/")
def home():
return "Hello!"آن خطی که با @ شروع میشود یک دکوریتور (decorator) است. دکوریتورها به شما اجازه میدهند بدون تغییر کدِ یک تابع، رفتاری به آن اضافه کنید؛ مثل لاگ گرفتن، زمانسنجی، کش کردن یا کنترل دسترسی. شاید جادویی به نظر برسند، اما از سه ایدهٔ ساده ساخته شدهاند. بیایید یکییکی سراغشان برویم.
ایدهٔ ۱: توابع شیء هستند#
در پایتون، توابع هم مثل هر مقدار دیگری هستند. میتوانید آنها را به متغیرها نسبت دهید و به توابع دیگر بفرستید:
def greet(name):
return f"Hello, {name}!"
say_hello = greet # no parentheses: we're not calling it
print(say_hello("Ada")) # Hello, Ada!ایدهٔ ۲: میتوان تابع را داخل تابع تعریف کرد#
def outer():
def inner():
return "I'm inside!"
return inner # return the function itself
fn = outer()
print(fn()) # I'm inside!ایدهٔ ۳: توابع داخلی محیط اطرافشان را به خاطر میسپارند#
یک تابع داخلی میتواند از متغیرهای تابعی که آن را ساخته استفاده کند، حتی بعد از اینکه آن تابع بیرونی تمام شده باشد. به این کلوژر (closure) میگویند:
def make_multiplier(factor):
def multiply(x):
return x * factor # "remembers" factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5)) # 10 15کنار هم گذاشتن: اولین دکوریتور شما#
دکوریتور تابعی است که یک تابع میگیرد و یک تابع جدید برمیگرداند؛ معمولاً تابعی که تابع اصلی را با رفتاری اضافه میپوشاند:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def greet(name):
return f"Hello, {name}!"
greet = shout(greet) # wrap it manually
print(greet("Ada")) # HELLO, ADA!سینتکس @ فقط میانبُری برای همان خطِ greet = shout(greet) است:
@shout
def greet(name):
return f"Hello, {name}!"
print(greet("Ada")) # HELLO, ADA!*args و **kwargs به wrapper اجازه میدهند هر آرگومانی را بپذیرد و مستقیم پاس بدهد، پس دکوریتور روی توابعی با هر امضایی کار میکند.
functools.wraps را فراموش نکنید#
دکوریتور ما یک مشکل ظریف دارد: تابعِ پوشاندهشده هویتش را از دست داده است.
print(greet.__name__) # wrapperاین مشکل دیباگ کردن، ابزارهای مستندسازی و بعضی فریمورکها را خراب میکند. با functools.wraps درستش کنید؛ این تابع نام، docstring و دیگر اطلاعات تابع اصلی را روی wrapper کپی میکند:
from functools import wraps
def shout(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapperعادتش کنید
@wraps(func) را به هر دکوریتوری که مینویسید اضافه کنید. فقط یک خط هزینه دارد و جلوی باگهای گیجکننده را در آینده میگیرد.
یک دکوریتور کاربردی: زمانسنجی#
بیایید چیزی مفید بسازیم: دکوریتوری که گزارش میدهد یک تابع چقدر طول میکشد.
import time
from functools import wraps
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.3f}s")
return wrapper
@timed
def build_report(rows):
return sum(i * i for i in range(rows))
build_report(5_000_000) # build_report took 0.312stry/finally مطمئن میشود که زمان حتی اگر تابع خطا بدهد چاپ شود.
دکوریتورهایی که آرگومان میگیرند#
اگر بخواهید @retry(times=3) بنویسید چه؟ آنوقت به یک لایهٔ دیگر نیاز دارید: تابعی که آرگومانها را میگیرد و یک دکوریتور برمیگرداند.
import time
from functools import wraps
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as error:
if attempt == times:
raise
print(f"Attempt {attempt} failed: {error}. Retrying in {delay}s…")
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_prices():
...@retry(times=5) اول retry(...) را صدا میزند که decorator را برمیگرداند و بعد آن روی fetch_prices اعمال میشود. سه لایه پیچیده به نظر میرسد، اما هر کدام فقط یک وظیفه دارد:
| لایه | دریافت میکند | برمیگرداند |
|---|---|---|
retry |
تنظیمات (times، delay…) |
decorator |
decorator |
تابعی که باید پوشانده شود | wrapper |
wrapper |
آرگومانهای هر فراخوانی | نتیجهٔ تابع |
دکوریتورهای داخلی که باید بشناسید#
پایتون چند دکوریتور عالیِ آماده دارد. مثلاً لازم نیست کش را خودتان بنویسید، چون functools.cache (پایتون ۳.۹ به بعد) این کار را برایتان انجام میدهد:
from functools import cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 354224848179261915075, instantlyبدون @cache، این بازگشتِ ساده بیش از ۱۰ به توان ۲۰ فراخوانی تابع انجام میداد. با آن، هر مقدار دقیقاً یک بار محاسبه میشود. اگر لازم است مصرف حافظه را محدود کنید، از functools.lru_cache(maxsize=…) استفاده کنید.
دکوریتورهای داخلیِ دیگری که ارزش شناختن دارند:
@propertyیک متد را به شکل یک ویژگیِ فقطخواندنی در اختیار میگذارد.@staticmethodو@classmethodمتدهایی تعریف میکنند که به نمونه (instance) نیاز ندارند.@dataclassمتدهای__init__،__repr__و__eq__را برایتان میسازد.@contextlib.contextmanagerیک generator را به context manager برای دستورwithتبدیل میکند.
جمعبندی#
- توابع شیء هستند: میتوان آنها را جابهجا کرد، برگرداند و داخل توابع دیگر تعریف کرد.
- دکوریتور یک تابع میگیرد و نسخهٔ پوشاندهشدهٔ آن را برمیگرداند؛
@decoخلاصهٔfunc = deco(func)است. - برای پشتیبانی از هر امضایی از
*args, **kwargsاستفاده کنید و همیشه@functools.wrapsرا به کار ببرید. - دکوریتورهایی که آرگومان میگیرند یک لایهٔ اضافه لازم دارند: تابعی که دکوریتور را برمیگرداند.
- پیش از نوشتن دکوریتور خودتان، سراغ نمونههای داخلی مثل
@cache،@propertyو@dataclassبروید.
چالش: یک دکوریتور @log_calls بنویسید که هر بار تابع صدا زده میشود، نام و آرگومانهایش را چاپ کند. بعد آن را همراه با @timed روی هم بگذارید و ببینید به چه ترتیبی اجرا میشوند.