پایتون سطح متوسط

دکوراتور در پایتون به زبان ساده؛ آموزش @decorator با مثال

دکوراتور در پایتون به زبان ساده با مثال

🗓 آخرین به‌روزرسانی:

دکوراتور (Decorator) در پایتون ابزاری است که رفتار یک تابع را بدون تغییر دادن کد داخل آن عوض می‌کند. اگر تا حالا علامت @ را بالای یک تابع دیده‌اید، مثل @property یا @app.route در فلسک، با دکوراتور روبه‌رو شده‌اید. دکوراتورها در نگاه اول جادویی به نظر می‌رسند، اما پشت آن‌ها فقط چند ایده ساده درباره تابع‌ها قرار دارد. در این آموزش دکوراتور در پایتون را به زبان ساده و قدم‌به‌قدم یاد می‌گیرید؛ از اینکه تابع در پایتون یک شیء است تا ساخت دکوراتورهای کاربردی مثل زمان‌سنج، کش، اعتبارسنجی و تلاش مجدد. همه کدها با پایتون 3.13 اجرا شده‌اند و خروجی زیر هر کد، خروجی واقعی است.

دکوراتور چیست؟ تعریف ساده

دکوراتور تابعی است که یک تابع دیگر را به‌عنوان ورودی می‌گیرد، یک تابع جدید می‌سازد که دور تابع اصلی «پیچیده» شده و همان تابع جدید را برمی‌گرداند. تابع جدید معمولاً کاری را قبل یا بعد از اجرای تابع اصلی انجام می‌دهد؛ مثلاً زمان اجرا را اندازه می‌گیرد، ورودی‌ها را بررسی می‌کند یا گزارش (log) می‌نویسد. نتیجه این است که منطق تکراری را یک بار می‌نویسید و با یک خط @ به هر تابعی که خواستید اضافه می‌کنید.

برای اینکه این تعریف معنا پیدا کند، باید سه ویژگی تابع‌ها در پایتون را بشناسیم: تابع را می‌توان در متغیر ریخت، می‌توان آن را به تابع دیگر داد و می‌توان آن را از تابع دیگر برگرداند.

دکوراتور چه مشکلی را حل می‌کند؟

فرض کنید ده تابع در برنامه دارید و می‌خواهید زمان اجرای همه آن‌ها را اندازه بگیرید. راه ساده این است که اول و آخر هر تابع دو خط کد زمان‌سنجی اضافه کنید؛ یعنی بیست خط تکراری. حالا اگر بخواهید قالب گزارش را عوض کنید، باید ده جا را ویرایش کنید و احتمال جا انداختن یکی از آن‌ها زیاد است. اصل DRY (خودت را تکرار نکن) می‌گوید منطق مشترک باید یک جا نوشته شود. دکوراتور دقیقاً همین کار را می‌کند: کد زمان‌سنجی را یک بار در دکوراتور می‌نویسید و با گذاشتن یک خط @timer بالای هر تابع، آن را اضافه یا حذف می‌کنید. تابع اصلی هم تمیز می‌ماند و فقط کار اصلی خودش را انجام می‌دهد.

به همین دلیل دکوراتورها در کتابخانه‌های بزرگ پایتون همه‌جا دیده می‌شوند. وقتی آن‌ها را بفهمید، خواندن کد فریم‌ورک‌هایی مثل Flask، Django و pytest هم بسیار ساده‌تر می‌شود، چون می‌دانید پشت هر @ یک تابع معمولی پایتون قرار دارد.

پیش‌نیاز ۱: تابع در پایتون یک شیء است

در پایتون تابع‌ها «شهروند درجه‌یک» هستند؛ یعنی مثل عدد و رشته می‌توان آن‌ها را جابه‌جا کرد. وقتی نام تابع را بدون پرانتز می‌نویسید، خود تابع را دارید، نه نتیجه اجرای آن را.

def greet(name):
    return f"Hello, {name}!"

say_hello = greet          # no parentheses: the function itself
print(say_hello("Sara"))

def apply(func, value):
    return func(value)

print(apply(greet, "Ali"))
print(apply(str.upper, "python"))
print(type(greet).__name__)

خروجی:

Hello, Sara!
Hello, Ali!
PYTHON
function

همین ایده را قبلاً در تابع lambda و توابعی مثل sorted(key=...) دیده‌اید: تابعی را به تابع دیگر می‌دهید تا آن را صدا بزند.

پیش‌نیاز ۲: تابع تودرتو و Closure

می‌توانید داخل یک تابع، تابع دیگری تعریف کنید و آن را برگردانید. تابع داخلی متغیرهای تابع بیرونی را «به خاطر می‌سپارد»، حتی بعد از اینکه اجرای تابع بیرونی تمام شده است. به این حالت Closure می‌گویند و موتور اصلی دکوراتورهاست.

def make_multiplier(factor):
    def multiply(number):
        return number * factor   # factor is remembered
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))
print(double.__closure__[0].cell_contents)

خروجی:

20 30
2

در خروجی می‌بینید که double و triple هر کدام مقدار factor مخصوص خودشان را نگه داشته‌اند، با اینکه تابع make_multiplier مدت‌ها پیش تمام شده است. خط آخر نشان می‌دهد این مقدار واقعاً داخل تابع ذخیره شده است. در دکوراتورها، همین مکانیزم باعث می‌شود تابع داخلی wrapper به تابع اصلی func دسترسی داشته باشد.

ساخت اولین دکوراتور، بدون علامت @

حالا همه قطعه‌ها را داریم. تابع announce یک تابع می‌گیرد، تابع داخلی wrapper را می‌سازد که قبل و بعد از اجرای تابع اصلی پیام چاپ می‌کند و wrapper را برمی‌گرداند. سپس نام تابع اصلی را به نسخه پیچیده‌شده اختصاص می‌دهیم.

def announce(func):
    def wrapper():
        print("Before", func.__name__)
        func()
        print("After", func.__name__)
    return wrapper

def backup():
    print("Backing up files...")

backup = announce(backup)   # manual decoration
backup()

خروجی:

Before backup
Backing up files...
After backup

دقت کنید که بعد از خط backup = announce(backup)، نام backup دیگر به تابع اصلی اشاره نمی‌کند، بلکه به wrapper اشاره می‌کند. تابع اصلی هنوز وجود دارد، اما فقط از داخل closure قابل دسترسی است. این نکته کلید فهم همه رفتارهای عجیب دکوراتورهاست.

نحو @: همان کار، خواناتر

نوشتن backup = announce(backup) بعد از هر تابع خسته‌کننده است و ممکن است فراموش شود. پایتون برای همین کار نحو کوتاه @ را دارد. نوشتن @announce بالای تعریف تابع دقیقاً معادل همان خط است؛ هیچ جادوی دیگری در کار نیست.

def announce(func):
    def wrapper():
        print("Before", func.__name__)
        func()
        print("After", func.__name__)
    return wrapper

@announce
def deploy():
    print("Deploying the website...")

deploy()

خروجی:

Before deploy
Deploying the website...
After deploy

دکوراتور برای هر تابع: *args، **kwargs و functools.wraps

دکوراتور قبلی فقط برای تابع‌های بدون آرگومان کار می‌کند و مقدار بازگشتی را هم دور می‌ریزد. نسخه درست باید با *args و **kwargs هر آرگومانی را بپذیرد و نتیجه تابع اصلی را return کند. مشکل دوم این است که تابع پیچیده‌شده نام و توضیح (docstring) خودش را از دست می‌دهد و همه‌جا wrapper دیده می‌شود. دکوراتور functools.wraps این اطلاعات را از تابع اصلی کپی می‌کند.

import functools

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

def proper(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@plain
def add(a, b):
    """Add two numbers."""
    return a + b

@proper
def multiply(a, b):
    """Multiply two numbers."""
    return a * b

print(add(2, 3), add.__name__, add.__doc__)
print(multiply(2, 3), multiply.__name__, multiply.__doc__)

خروجی:

5 wrapper None
6 multiply Multiply two numbers.

در خط اول خروجی، تابع add نامش را به wrapper باخته و docstring آن None شده است. در خط دوم، به لطف wraps، تابع multiply نام و توضیح خودش را حفظ کرده است. علاوه بر این، wraps ویژگی __wrapped__ را هم اضافه می‌کند که در صورت نیاز با آن به تابع اصلی بدون دکوراتور دسترسی دارید.

قانون ساده: همیشه در دکوراتورهایتان از @functools.wraps(func) استفاده کنید. بدون آن، ابزارهای دیباگ، مستندسازی و تست‌نویسی نام اشتباه نشان می‌دهند و پیدا کردن خطا سخت می‌شود.

الگوی استاندارد دکوراتور

تقریباً همه دکوراتورهای واقعی از این الگو پیروی می‌کنند. آن را حفظ کنید و هر بار فقط بخش «قبل» و «بعد» را تغییر دهید:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # code before the call
        result = func(*args, **kwargs)
        # code after the call
        return result
    return wrapper

مثال کاربردی ۱: اندازه‌گیری زمان اجرا

یکی از رایج‌ترین دکوراتورها، زمان‌سنج است. با time.perf_counter زمان را قبل و بعد از اجرا می‌گیریم و اختلاف را چاپ می‌کنیم. این دکوراتور را می‌توانید روی هر تابع کندی بگذارید تا بفهمید گلوگاه برنامه کجاست.

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__} took {elapsed:.1f}s")
        return result
    return wrapper

@timer
def slow_sum(n):
    time.sleep(0.2)
    return sum(range(n))

print("Result:", slow_sum(1_000))

خروجی:

slow_sum took 0.2s
Result: 499500

دکوراتور با آرگومان

گاهی می‌خواهید خود دکوراتور تنظیم‌پذیر باشد؛ مثلاً @repeat(3) تابع را سه بار اجرا کند. در این حالت یک لایه دیگر لازم است: تابع بیرونی آرگومان دکوراتور را می‌گیرد و خود دکوراتور را برمی‌گرداند. پس @repeat(3) ابتدا repeat(3) را اجرا می‌کند و نتیجه آن، یعنی یک دکوراتور معمولی، روی تابع اعمال می‌شود.

import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            results = []
            for _ in range(times):
                results.append(func(*args, **kwargs))
            return results
        return wrapper
    return decorator

@repeat(3)
def roll(label):
    return f"{label}!"

print(roll("Go"))

خروجی:

['Go!', 'Go!', 'Go!']

مثال کاربردی ۲: کش کردن نتیجه با lru_cache

پایتون دکوراتورهای آماده‌ای هم دارد. functools.lru_cache نتیجه تابع را برای هر ورودی ذخیره می‌کند تا محاسبه تکراری انجام نشود. تابع بازگشتی فیبوناچی بدون کش برای ورودی‌های بزرگ بسیار کند است، اما با یک خط دکوراتور فوراً اجرا می‌شود. اگر با بازگشت آشنا نیستید، آموزش تابع بازگشتی در پایتون و برای رفع خطای عمق بازگشت، RecursionError و تابع بازگشتی را ببینید.

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(80))
print(fib.cache_info())

خروجی:

23416728348467685
CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)

خروجی cache_info نشان می‌دهد تابع فقط ۸۱ بار واقعاً اجرا شده (misses) و ۷۸ بار جواب از کش آمده (hits). بدون کش، محاسبه fib(80) به میلیاردها فراخوانی نیاز داشت و عملاً تمام نمی‌شد.

از پایتون 3.9 به بعد @functools.cache هم وجود دارد که معادل lru_cache(maxsize=None) است. فقط تابع‌هایی را کش کنید که برای ورودی یکسان همیشه خروجی یکسان می‌دهند و اثر جانبی (مثل نوشتن فایل) ندارند.

مثال کاربردی ۳: اعتبارسنجی ورودی

دکوراتورها برای بررسی‌های تکراری ورودی عالی‌اند. دکوراتور زیر مطمئن می‌شود همه آرگومان‌های عددی مثبت باشند و در غیر این صورت ValueError با پیام روشن ایجاد می‌کند. این‌طور منطق اعتبارسنجی از منطق اصلی تابع جدا می‌ماند.

import functools

def positive_only(func):
    @functools.wraps(func)
    def wrapper(*args):
        for value in args:
            if value <= 0:
                raise ValueError(f"{func.__name__}() needs positive numbers, got {value}")
        return func(*args)
    return wrapper

@positive_only
def rectangle_area(width, height):
    return width * height

print(rectangle_area(4, 5))
try:
    rectangle_area(4, -2)
except ValueError as error:
    print("Error:", error)

خروجی:

20
Error: rectangle_area() needs positive numbers, got -2

مثال کاربردی ۴: تلاش مجدد (Retry)

در کار با شبکه، API یا وب اسکرپینگ، گاهی یک درخواست به‌طور موقت شکست می‌خورد. دکوراتور retry تابع را چند بار دوباره اجرا می‌کند و فقط اگر همه تلاش‌ها شکست خورد، خطا را بالا می‌فرستد. در مثال زیر یک تابع ساختگی دو بار اول شکست می‌خورد تا رفتار دکوراتور دیده شود. درباره مدیریت خطا آموزش try/except را بخوانید.

import functools

def retry(attempts=3, exceptions=(ConnectionError,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as error:
                    print(f"Attempt {attempt} failed: {error}")
            raise RuntimeError(f"{func.__name__} failed after {attempts} attempts")
        return wrapper
    return decorator

calls = {"count": 0}

@retry(attempts=3)
def fetch_data():
    calls["count"] += 1
    if calls["count"] < 3:
        raise ConnectionError("server busy")
    return "data received"

print(fetch_data())

خروجی:

Attempt 1 failed: server busy
Attempt 2 failed: server busy
data received

چند دکوراتور روی یک تابع: ترتیب مهم است

می‌توانید چند دکوراتور را روی هم بگذارید. دکوراتوری که به تابع نزدیک‌تر است (پایین‌تر) اول اعمال می‌شود، اما هنگام اجرا، لایه بیرونی (بالاتر) اول کارش را شروع می‌کند. مثل جعبه‌هایی که داخل هم گذاشته شده‌اند:

import functools

def tag(name):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return f"<{name}>{func(*args, **kwargs)}</{name}>"
        return wrapper
    return decorator

@tag("b")
@tag("i")
def title(text):
    return text

print(title("Python"))

خروجی:

<b><i>Python</i></b>

اینجا ابتدا @tag("i") روی title اعمال شده و نتیجه آن داخل @tag("b") قرار گرفته است. به همین دلیل تگ b بیرون و تگ i داخل است. وقتی دکوراتورهای زمان‌سنج و گزارش‌گیری را ترکیب می‌کنید، این ترتیب تعیین می‌کند که زمان‌سنج زمان گزارش‌گیری را هم حساب کند یا نه.

دکوراتور با کلاس و دکوراتورهای داخلی پایتون

دکوراتور لازم نیست حتماً تابع باشد؛ هر شیء قابل فراخوانی کار می‌کند. کلاسی که متد __call__ دارد می‌تواند وضعیت نگه دارد؛ مثلاً تعداد دفعات اجرای تابع را بشمارد. علاوه بر این، پایتون سه دکوراتور داخلی پرکاربرد برای کلاس‌ها دارد: @property برای تبدیل متد به ویژگی، @staticmethod برای متدی که به نمونه نیاز ندارد و @classmethod برای متدی که روی خود کلاس کار می‌کند. اگر با کلاس‌ها آشنا نیستید، کلاس و شیء در پایتون را ببینید.

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 ping():
    return "pong"

ping(); ping(); ping()
print(ping.__name__, "called", ping.calls, "times")

class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @property
    def fahrenheit(self):
        return self.celsius * 9 / 5 + 32

    @classmethod
    def freezing(cls):
        return cls(0)

    @staticmethod
    def is_valid(value):
        return value >= -273.15

t = Temperature.freezing()
print(t.fahrenheit, Temperature.is_valid(-300))

خروجی:

ping called 3 times
32.0 False

کجاها از دکوراتور استفاده می‌شود؟

  • فریم‌ورک‌های وب: در Flask و FastAPI مسیرها با @app.route یا @app.get تعریف می‌شوند و در Django با @login_required دسترسی محدود می‌شود. برای شروع کار با این فریم‌ورک‌ها، آموزش Flask و ساخت اولین وب‌سایت با پایتون را ببینید.
  • ربات‌ها: کتابخانه‌های ربات تلگرام برای ثبت دستورها از دکوراتور استفاده می‌کنند؛ نمونه را در ساخت ربات تلگرام با پایتون ببینید.
  • تست‌نویسی: در pytest دکوراتورهایی مثل @pytest.mark.parametrize یک تست را با داده‌های مختلف اجرا می‌کنند.
  • کارایی و پایداری: کش، زمان‌سنجی، محدودیت تعداد درخواست و تلاش مجدد.
  • کلاس‌ها: @property، @dataclass و @classmethod.

اشتباهات رایج در نوشتن دکوراتور

  • فراموش کردن return در wrapper: تابع دکوراتورشده همیشه None برمی‌گرداند. نتیجه func(*args, **kwargs) را برگردانید.
  • صدا زدن تابع در return دکوراتور: باید return wrapper بنویسید، نه return wrapper()؛ در غیر این صورت تابع هنگام تعریف اجرا می‌شود.
  • نداشتن functools.wraps: نام و docstring تابع گم می‌شود.
  • اشتباه در دکوراتور با آرگومان: نوشتن @repeat به جای @repeat(3) باعث می‌شود خود تابع به جای عدد به repeat داده شود و خطای TypeError بگیرید.
  • استفاده بیش از حد: دکوراتور را برای منطق مشترک و تکراری نگه دارید؛ اگر فقط یک تابع به آن نیاز دارد، کد ساده داخل همان تابع خواناتر است.

تمرین

یک دکوراتور به نام log_calls بنویسید که قبل از اجرای تابع، نام آن و آرگومان‌هایش را چاپ کند و بعد از اجرا، مقدار بازگشتی را نشان دهد. سپس آن را با timer ترکیب کنید و ببینید ترتیب دکوراتورها چه اثری روی خروجی دارد. در قدم بعد نسخه‌ای با آرگومان بسازید که بتوان گزارش را روشن و خاموش کرد، مثل @log_calls(enabled=False). تمرین‌های بیشتر را در ۵۰ تمرین پایتون با جواب پیدا می‌کنید.

سوالات متداول

دکوراتور در پایتون به زبان ساده چیست؟

تابعی است که یک تابع دیگر را می‌گیرد، رفتاری را قبل یا بعد از اجرای آن اضافه می‌کند و تابع جدید را برمی‌گرداند؛ بدون اینکه کد تابع اصلی تغییر کند.

علامت @ در پایتون چه کاری انجام می‌دهد؟

نوشتن @decorator بالای تعریف تابع معادل نوشتن func = decorator(func) بعد از تعریف آن است؛ فقط نحو کوتاه‌تر و خواناتری است.

چرا باید از functools.wraps استفاده کنم؟

چون بدون آن تابع دکوراتورشده نام، docstring و اطلاعات تابع اصلی را از دست می‌دهد و در دیباگ و مستندسازی به شکل wrapper دیده می‌شود.

دکوراتور با آرگومان چطور کار می‌کند؟

یک لایه تابع اضافه دارد: تابع بیرونی آرگومان‌ها را می‌گیرد و یک دکوراتور معمولی برمی‌گرداند که روی تابع اعمال می‌شود؛ مثل @repeat(3).

ترتیب چند دکوراتور روی یک تابع چگونه است؟

دکوراتور پایینی اول اعمال می‌شود و لایه داخلی است، اما هنگام اجرا لایه بالایی (بیرونی) اول کارش را شروع می‌کند.

آموزش‌های مرتبط

آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.
🧭 مسیر یادگیری پایتون از صفر
ماژول 9: برنامه‌نویسی شیءگرا — درس 41 از 44

برای ذخیره پیشرفت در همه دستگاه‌ها و دریافت گواهی پایان مسیر، رایگان ثبت‌نام کن یا وارد شو.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *