ArrayList و HashMap در جاوا؛ آموزش Collections با مثال
آرایه در جاوا اندازه ثابت دارد، اما دادههای برنامه واقعی مدام بزرگ و کوچک میشوند. Collections Framework با ArrayList، HashMap، …
دکوریتور در پایتون تابعی است که تابع دیگری را میگیرد و بدون تغییر کد آن، رفتاری به آن اضافه میکند. در این آموزش از پایه شروع میکنیم، اولین دکوریتور را میسازیم، با functools.wraps و دکوریتور با آرگومان کار میکنیم و کاربرد واقعی آن را در جنگو میبینیم. همه مثالها را با پایتون ۳٫۱۵ اجرا کردهایم.
آخرین بهروزرسانی: ۲۰ آبان ۱۴۰۵ — منابع بررسیشده در این روز: کتابخانه functools و واژهنامه پایتون، بخش تعریف تابع در مرجع زبان، PEP 318 و PEP 614، و مستندات دکوریتورهای ویو و احراز هویت جنگو ۶٫۱.
⚡ پاسخ کوتاه
دکوریتور تابعی است که یک تابع را میگیرد و تابع تازهای برمیگرداند؛ نوشتن @my_decorator بالای def دقیقاً یعنی func = my_decorator(func). برای نوشتن دکوریتور درست، داخل آن یک wrapper با *args و **kwargs بسازید، نتیجه تابع اصلی را return کنید و wrapper را با functools.wraps بپوشانید تا نام و توضیح تابع گم نشود.
📑 فهرست مطالب
دکوریتور تابعی است که یک تابع دیگر را میگیرد، رفتاری به آن اضافه میکند و تابع تازهای برمیگرداند؛ بدون اینکه کد داخل تابع اصلی را تغییر دهید. علامت @ که بالای تعریف تابع میبینید، فقط یک میانبر برای همین کار است. واژهنامه رسمی پایتون هم دکوریتور را همینطور تعریف میکند: تابعی که تابع دیگری را برمیگرداند و معمولاً با نحو @wrapper به کار میرود. اگر با جنگو کار کرده باشید، دکوریتور را حتماً دیدهاید: login_required بالای یک ویو میگوید فقط کاربر واردشده اجازه دیدن صفحه را دارد. در خود پایتون هم property، staticmethod، classmethod و functools.cache همه دکوریتورند. در این آموزش از پایه شروع میکنیم، دکوریتور واقعی میسازیم، با functools.wraps مشکل گم شدن نام تابع را حل میکنیم، دکوریتور با آرگومان مینویسیم و در آخر کاربردش را در جنگو میبینیم. همه مثالهای این مقاله را با پایتون 3.15.2، آخرین نسخه پایدار در روز نوشتن، اجرا کردهایم و خروجیها واقعیاند. مثالهای جنگو هم با Django 6.1.3 آزمایش شدهاند. اگر تازه با پایتون آشنا شدهاید، تابع map در پایتون و لیست کامپریهنشن در پایتون پیشزمینه خوبی برای کار با تابعها میدهند.
برای فهم دکوریتور، اول باید یک واقعیت ساده را بپذیریم: در پایتون تابع هم مثل عدد و رشته یک شیء است. میشود آن را در متغیر گذاشت، به تابع دیگری فرستاد و از تابع دیگری برگرداند. به این ویژگی میگویند تابعهای درجهاول. وقتی بدون پرانتز نام تابع را مینویسید، خود تابع را دارید، نه نتیجه اجرای آن را. یک مفهوم دیگر هم لازم داریم: تابع تودرتو. میشود داخل یک تابع، تابع دیگری تعریف کرد. تابع داخلی به متغیرهای تابع بیرونی دسترسی دارد و حتی بعد از تمام شدن اجرای تابع بیرونی هم آنها را به یاد میآورد. به این حالت کلوژر یا closure میگویند. دکوریتور دقیقاً از ترکیب این دو ساخته میشود: تابعی که تابع میگیرد، داخلش یک تابع پوشاننده میسازد و همان پوشاننده را برمیگرداند.

فرض کنید چند تابع دارید که متن برمیگردانند و میخواهید خروجی بعضیشان با حروف بزرگ باشد. بهجای تغییر تکتک توابع، یک دکوریتور مینویسیم:
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return "hello"
loud = shout(greet)
print(greet())
print(loud())
@shout
def welcome():
return "welcome to raadino"
print(welcome())
print(welcome.__name__)
# output:
# hello
# HELLO
# WELCOME TO RAADINO
# wrapper
خط loud = shout(greet) همه ماجرا را نشان میدهد. shout تابع greet را گرفت و تابع wrapper را برگرداند. وقتی loud را صدا میزنیم، در واقع wrapper اجرا میشود؛ wrapper تابع اصلی را صدا میزند و نتیجه را بزرگ میکند. تابع greet خودش دستنخورده ماند. نوشتن @shout بالای welcome دقیقاً یعنی welcome = shout(welcome). سند PEP 318 که این نحو را در پایتون ۲٫۴ اضافه کرد، توضیح میدهد که هدف، خواناتر کردن همین الگو بود؛ چون قبلاً باید تبدیل را بعد از بدنه تابع مینوشتید و خواننده تا آخر تابع نمیفهمید چه چیزی به آن اضافه شده است. از پایتون ۳٫۹ هم طبق PEP 614 بعد از @ میشود هر عبارت معتبر پایتون نوشت، نه فقط نام ساده. اما خط آخر خروجی یک مشکل را نشان میدهد: نام welcome حالا wrapper است. این مشکل را کمی بعد حل میکنیم.
یک نکته مهم که مبتدیها معمولاً اشتباه میکنند: بدنه خود دکوریتور، یعنی کد shout، فقط یک بار و در لحظه تعریف تابع اجرا میشود؛ همان وقتی که پایتون به خط @shout میرسد. اما wrapper هر بار که تابع را صدا میزنید اجرا میشود. پس اگر کاری باید در هر فراخوانی انجام شود، جایش داخل wrapper است، نه بیرون آن.
دکوریتور اول فقط برای تابعی کار میکرد که هیچ ورودی نداشت. در عمل باید دکوریتوری بنویسیم که روی هر تابعی با هر تعداد آرگومان کار کند. راهحل *args و **kwargs است: wrapper هر چه گرفت، دستنخورده به تابع اصلی میدهد. نتیجه تابع اصلی را هم حتماً return کنید؛ فراموش کردن همین return باعث میشود تابع دکورشده همیشه None برگرداند.
وقتی تابع را با wrapper عوض میکنیم، نام، توضیح (docstring) و اطلاعات دیگر تابع اصلی گم میشود. این فقط یک مسئله ظاهری نیست. ابزارهای دیباگ، لاگها، مستندسازی خودکار و حتی بعضی چارچوبها به این اطلاعات تکیه میکنند. ماژول functools برای همین دکوریتوری به نام wraps دارد که آن را روی خود wrapper میگذارید. حالا یک دکوریتور زمانسنج کامل بسازیم:
import functools
import time
def timer(func):
@functools.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
@timer
def slow_sum(n, pause=0.2):
"""Add numbers from 1 to n."""
time.sleep(pause)
return sum(range(1, n + 1))
print(slow_sum(100))
print(slow_sum.__name__, "|", slow_sum.__doc__)
print(slow_sum.__wrapped__(10, pause=0))
# output:
# slow_sum took 0.200s
# 5050
# slow_sum | Add numbers from 1 to n.
# 55
چند نکته در این کد هست. اول، به لطف wraps، نام و docstring تابع اصلی سر جایش ماند. طبق مستندات functools، wraps علاوه بر کپی کردن این ویژگیها، یک ویژگی به نام __wrapped__ هم اضافه میکند که به تابع اصلی اشاره دارد. خط آخر نشان میدهد با __wrapped__ میشود تابع را بدون زمانسنج صدا زد؛ این در نوشتن تست خیلی به کار میآید. دوم، finally باعث میشود زمان حتی وقتی تابع خطا داد هم چاپ شود. سوم، عدد زمان در هر اجرا کمی فرق میکند؛ ۰٫۲۰۰ ثانیه خروجی اجرای ما بود. قاعده ساده: هر دکوریتوری که مینویسید، روی wrapper آن @functools.wraps(func) بگذارید. هزینهای ندارد و بعداً جلوی دردسرهای زیادی را میگیرد.

گاهی میخواهید رفتار دکوریتور را تنظیم کنید؛ مثلاً بگویید یک عملیات شبکهای چند بار دوباره تلاش شود. اینجا یک لایه دیگر لازم است: تابعی که آرگومانها را میگیرد و خود دکوریتور را برمیگرداند. در واقع @retry(times=3) اول retry را با آرگومان صدا میزند و نتیجه آن، که یک دکوریتور معمولی است، روی تابع اعمال میشود.
import functools
def retry(times=3, 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 as exc:
print(f"attempt {attempt} failed: {exc}")
if attempt == times:
raise
return wrapper
return decorator
calls = {"count": 0}
@retry(times=3, exceptions=(ConnectionError,))
def fetch_price():
calls["count"] += 1
if calls["count"] < 3:
raise ConnectionError("server busy")
return 125_000
print(fetch_price())
# output:
# attempt 1 failed: server busy
# attempt 2 failed: server busy
# 125000
تابع fetch_price دو بار اول خطای اتصال میدهد و بار سوم موفق میشود. دکوریتور خطا را گرفت، گزارش داد و دوباره تلاش کرد. اگر هر سه تلاش شکست میخورد، raise همان خطای آخر را بیرون میفرستاد تا گم نشود. دقت کنید که فقط ConnectionError را دوباره تلاش میکنیم. گرفتن همه خطاها با Exception معمولاً ایده بدی است، چون خطاهای برنامهنویسی مثل TypeError را هم پنهان میکند و بیدلیل تکرار میکند. در کد واقعی بین تلاشها کمی صبر هم اضافه کنید تا سرور شلوغ را بیشتر تحت فشار نگذارید. اشتباه رایج این است که دکوریتور با آرگومان را بدون پرانتز بنویسید؛ یعنی @retry بهجای @retry(). در این حالت پایتون خود تابع شما را بهعنوان آرگومان times به retry میدهد و نتیجه یک تابع خراب است. اگر میخواهید هر دو شکل کار کند، باید داخل دکوریتور بررسی کنید که اولین آرگومان تابع است یا نه؛ اما برای شروع، همیشه پرانتز بگذارید.
میشود چند دکوریتور را روی یک تابع گذاشت. دکوریتوری که به تابع نزدیکتر است، اول اعمال میشود. یعنی نوشتن @a و @b بالای f برابر است با f = a(b(f)). در زمان اجرا هم لایه بیرونی، یعنی a، زودتر از همه اجرا میشود.
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
def require_positive(func):
@functools.wraps(func)
def wrapper(amount):
if amount <= 0:
raise ValueError("amount must be positive")
return func(amount)
return wrapper
@log_calls
@require_positive
def deposit(amount):
return f"deposited {amount}"
print(deposit(50))
try:
deposit(-5)
except ValueError as e:
print("error:", e)
# output:
# calling deposit
# deposited 50
# calling deposit
# error: amount must be positive
چون log_calls بیرونی است، حتی فراخوانی نامعتبر هم ثبت شد و بعد require_positive جلوی آن را گرفت. اگر ترتیب را برعکس کنید، فراخوانی نامعتبر قبل از ثبت شدن رد میشود. هیچکدام ذاتاً غلط نیست؛ باید بدانید کدام را میخواهید. همین اصل در جنگو هم مهم است و در ادامه میبینیم.
لازم نیست همه دکوریتورها را خودتان بنویسید. کتابخانه استاندارد پایتون چند دکوریتور پرکاربرد دارد که احتمالاً هر روز با آنها کار میکنید:
| دکوریتور | کار | نمونه کاربرد |
|---|---|---|
| @property | تبدیل متد به ویژگی فقطخواندنی یا کنترلشده | قیمت نهایی محصول |
| @staticmethod | متدی که به self یا cls نیاز ندارد | تابع کمکی داخل کلاس |
| @classmethod | متدی که کلاس را بهعنوان cls میگیرد | سازنده جایگزین مثل from_string |
| @functools.cache | کش کردن نتیجه برای ورودی تکراری | محاسبه بازگشتی سنگین |
| @functools.lru_cache | کش با سقف اندازه | کش محدود برای داده زیاد |
| @functools.wraps | حفظ نام و توضیح تابع اصلی در wrapper | هر دکوریتوری که خودتان مینویسید |
| @dataclasses.dataclass | ساخت خودکار __init__، __repr__ و __eq__ | کلاسهای داده ساده |
مثال زیر سه دکوریتور کلاس و functools.cache را نشان میدهد. محاسبه عدد هشتادم فیبوناچی بدون کش با این روش بازگشتی عملاً تمام نمیشود، اما با cache هر عدد فقط یک بار محاسبه میشود:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80))
print(fib.cache_info())
class Product:
def __init__(self, price, tax_rate):
self.price = price
self.tax_rate = tax_rate
@property
def final_price(self):
return round(self.price * (1 + self.tax_rate))
@staticmethod
def toman(rial):
return rial // 10
@classmethod
def tax_free(cls, price):
return cls(price, 0)
p = Product(100_000, 0.10)
print(p.final_price, Product.toman(50_000), Product.tax_free(80_000).final_price)
# output:
# 23416728348467685
# CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)
# 110000 5000 80000
cache_info نشان میدهد ۸۱ مقدار متفاوت محاسبه و ۷۸ بار از کش جواب داده شده است. یادتان باشد cache اندازه محدود ندارد؛ برای ورودیهای زیاد و متنوع، lru_cache با maxsize انتخاب امنتری است. همچنین آرگومانهای تابع کششده باید hashable باشند؛ یعنی لیست و دیکشنری را نمیشود مستقیم به آن داد. اگر با enum کار میکنید، کاربرد enum در پایتون نشان میدهد چرا عضوهای enum برای این کار مناسباند. property هم نشان میدهد دکوریتور فقط برای اضافه کردن لاگ نیست؛ میتواند شکل استفاده از یک متد را عوض کند. final_price را مثل یک ویژگی معمولی و بدون پرانتز میخوانیم، اما هر بار محاسبه میشود.
دکوریتور حتماً تابع نیست؛ هر شیء قابل فراخوانی میتواند دکوریتور باشد. اگر دکوریتور شما باید وضعیتی مثل تعداد فراخوانی را نگه دارد، یک کلاس با متد __call__ خواناتر است. در کلاس بهجای wraps از functools.update_wrapper استفاده میکنیم که همان کار را انجام میدهد:
import functools
class CallCounter:
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)
@CallCounter
def say(text):
return text
say("a"); say("b")
print(say.calls, say.__name__)
# output:
# 2 say
یک محدودیت مهم: دکوریتور کلاسی ساده مثل CallCounter روی متد کلاسها درست کار نمیکند، چون self شیء صاحب متد را دریافت نمیکند. برای متدها همان دکوریتور تابعی با wraps سادهتر است. طرف دیگر ماجرا دکوریتورهایی است که روی خود کلاس گذاشته میشوند. مشهورترینش dataclasses.dataclass است که از روی فیلدهای کلاس، متدهای __init__، __repr__ و __eq__ را میسازد. پس وقتی @dataclass را بالای کلاس میبینید، همان منطق همیشگی برقرار است: کلاس به یک تابع داده شده و نسخه تکمیلشدهاش برگشته است.
اگر تابع شما با async def تعریف شده، دکوریتور معمولی فقط یک coroutine برمیگرداند و زمان واقعی اجرا را نمیسنجد؛ چون کار اصلی بعداً و با await انجام میشود. برای چنین توابعی، wrapper را هم async بنویسید و تابع اصلی را await کنید:
import asyncio
import functools
import time
def async_timer(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
print(f"{func.__name__}: {time.perf_counter() - start:.2f}s")
return result
return wrapper
@async_timer
async def load():
await asyncio.sleep(0.1)
return "ok"
print(asyncio.run(load()))
# output:
# load: 0.10s
# ok
جنگو یکی از بهترین جاها برای دیدن دکوریتور در عمل است. مستندات جنگو برای ویوها چند خانواده دکوریتور دارد: محدود کردن متد HTTP مثل require_POST و require_http_methods، کنترل کش مثل cache_control و never_cache، و فشردهسازی با gzip_page. login_required هم در ماژول احراز هویت قرار دارد. اینها دقیقاً همان الگوی wrapper هستند: قبل از اجرای ویو شرطی را بررسی میکنند و اگر برقرار نبود، پاسخ دیگری برمیگردانند.
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST, require_http_methods
@login_required
@require_POST
def publish_post(request, pk):
return HttpResponse(f"post {pk} published")
@require_http_methods(["GET", "HEAD"])
def ping(request):
return HttpResponse("pong")
@method_decorator(login_required, name="dispatch")
class Dashboard(View):
def get(self, request):
return HttpResponse("dashboard")
این ویوها را با کلاینت آزمایشی جنگو امتحان کردیم. نتیجهها این بود: درخواست POST کاربر مهمان به publish_post با کد 302 به صفحه ورود هدایت شد. همان کاربر بعد از ورود، با GET کد 405 گرفت، چون require_POST فقط POST را میپذیرد، و با POST پیام موفقیت را دید. ping به POST جواب 405 و به GET جواب 200 داد و داشبورد هم برای کاربر مهمان 302 برگرداند. به ترتیب publish_post دقت کنید. login_required بیرونی است، پس اول ورود بررسی میشود و کاربر مهمان، حتی با متد اشتباه، به صفحه ورود میرود. اگر ترتیب برعکس بود، کاربر مهمان با GET خطای 405 میدید. این همان قانون ترتیب دکوریتورهاست که بالاتر دیدیم. برای ویوهای کلاسی، دکوریتور تابعی را نمیشود مستقیم روی متد گذاشت، چون متد اول self را میگیرد، نه request را. method_decorator جنگو این تبدیل را انجام میدهد. با name="dispatch" دکوریتور روی همه متدهای HTTP کلاس اعمال میشود. تفاوت ویوهای تابعی و کلاسی را در ویو و URL در جنگو مفصل توضیح دادهایم. اگر API میسازید، در آموزش Django REST Framework هم دکوریتورهایی مثل api_view را میبینید. وقتی پروژه آماده شد، مراحل انتشار آن روی سرور را در دیپلوی جنگو روی سرور لینوکس قدمبهقدم آوردهایم.

| اشتباه | نتیجه | راه درست |
|---|---|---|
| return نکردن نتیجه در wrapper | تابع تزئینشده همیشه None برمیگرداند | return func(*args, **kwargs) |
| فراموش کردن functools.wraps | نام و docstring تابع به wrapper تغییر میکند | @functools.wraps(func) روی wrapper |
| نوشتن wrapper بدون *args و **kwargs | تابع با آرگومان TypeError میدهد | پذیرفتن همه آرگومانها و عبور دادنشان |
| جا انداختن پرانتز در دکوریتور با آرگومان | @retry بهجای @retry(3) و رفتار اشتباه | دکوریتور کارخانهای را با پرانتز صدا بزنید |
| صدا زدن تابع بهجای برگرداندن آن | تابع هنگام تعریف اجرا میشود | return wrapper، نه return wrapper() |
| دکوریتور تابعی مستقیم روی متد ویو کلاسی | self بهجای request دریافت میشود | method_decorator در جنگو |
دکوریتور ابزار قدرتمندی است، اما هر تکراری دکوریتور نمیخواهد. اگر رفتار اضافهشده فقط در یک تابع لازم است، همان را داخل تابع بنویسید. اگر دکوریتور ورودی و خروجی تابع را به شکل غیرمنتظره تغییر میدهد، خواننده کد با دیدن امضای تابع گمراه میشود. و اگر برای فهم یک تابع باید پنج دکوریتور تودرتو را دنبال کنید، احتمالاً طراحی بیش از حد پیچیده شده است. یک معیار ساده: دکوریتور برای کارهای «حاشیهای» و تکراری عالی است؛ مثل لاگ، زمانسنجی، کش، کنترل دسترسی، اعتبارسنجی ورودی و تلاش دوباره. منطق اصلی کسبوکار باید داخل خود تابع و قابل دیدن بماند. این همان جداسازی مسئولیتهاست که در معماری تمیز هم به آن تکیه میکنیم. برای آمادگی مصاحبه هم دکوریتور از پرسشهای پرتکرار پایتون است؛ ۵۰ سؤال مصاحبه جنگو را هم ببینید.
تمرین اول: دکوریتوری به نام count_calls بنویسید که تعداد فراخوانی هر تابع را در ویژگی calls همان تابع نگه دارد. از functools.wraps استفاده کنید و بعد بررسی کنید که __name__ درست مانده است. تمرین دوم: دکوریتور با آرگومانی به نام only_types بنویسید که نوع آرگومان اول تابع را بررسی کند و اگر نوعش مجاز نبود، TypeError بدهد. آن را با @only_types(int, float) روی یک تابع محاسبه مالیات امتحان کنید. تمرین سوم: retry این مقاله را طوری تغییر دهید که بین تلاشها صبر کند و زمان صبر هر بار دو برابر شود. بعد با __wrapped__ تابع اصلی را بدون retry آزمایش کنید.
تابعی است که یک تابع میگیرد و تابع تازهای برمیگرداند که رفتار اضافه دارد؛ مثل لاگ گرفتن یا بررسی دسترسی. نوشتن @decorator بالای تابع f دقیقاً یعنی f = decorator(f).
نام، docstring و چند ویژگی دیگر تابع اصلی را روی تابع پوشاننده کپی میکند و ویژگی __wrapped__ را برای دسترسی به تابع اصلی اضافه میکند. بدون آن، تابع دکورشده نام wrapper میگیرد و ابزارهای دیباگ و مستندسازی گمراه میشوند.
نزدیکترین دکوریتور به تابع اول اعمال میشود و بیرونیترین دکوریتور در زمان اجرا اول اجرا میشود. @a و @b بالای f یعنی a(b(f)).
ارثبری رفتار را از طریق رابطه بین کلاسها اضافه میکند. دکوریتور رفتار را دور یک تابع یا کلاس مشخص میپیچد، بدون اینکه رابطه نوعی تازه بسازد. برای کارهای حاشیهای مثل لاگ و کش، دکوریتور معمولاً سادهتر است.
هر لایه دکوریتور یک فراخوانی تابع اضافه دارد که برای بیشتر برنامهها ناچیز است. اما اگر تابعی میلیونها بار در یک حلقه صدا زده میشود، این هزینه را با ابزاری مثل timeit اندازه بگیرید و بعد تصمیم بگیرید.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.