المزخرفات (decorators) في Python: ما هي وكيف تعمل وكيف تكتب مزخرفاً خاصاً
المزخرف في Python دالة تستقبل دالة أخرى وتعيد دالة جديدة، عادةً غلافاً حول الأصلية، يضيف سلوكاً قبل الاستدعاء أو بعده، مثل التسجيل وقياس الوقت والتخزين المؤقت. يُكتب بالرمز @ فوق تعريف الدالة، ويُفضّل أن يستخدم functools.wraps للحفاظ على اسم الدالة وتوثيقها.
المزخرف (decorator) في Python دالة تستقبل دالة أخرى وتعيد دالة جديدة، وغالباً ما تكون غلافاً حول الدالة الأصلية. بهذه الطريقة تُضاف إلى الدالة سلوكيات مثل التسجيل وقياس الوقت والتخزين المؤقت وإعادة المحاولة، دون تغيير كودها. يُكتب المزخرف بالرمز @اسم فوق تعريف الدالة، وتجده في كل مكان تقريباً: في Flask وFastAPI وpytest، وفي المكتبة القياسية.
ما هو المزخرف بكلمات بسيطة
تخيّل هدية وورق التغليف. تبقى الهدية (الدالة) كما هي، لكن تظهر من الخارج طبقة تعمل أولاً. المزخرف هو هذا التغليف: عند الاستدعاء ينفَّذ كود الغلاف أولاً، ثم يستدعي الدالة الأصلية، ويستطيع أن يفعل شيئاً بالنتيجة.
وهذا ممكن لأن الدوال في Python كائنات عادية. يمكن تمريرها إلى دوال أخرى، وإعادتها من الدوال، وتعريفها داخل دوال. وعلى هذه الخصائص الثلاث تقوم المزخرفات.
كيف يعمل المزخرف: الرمز @ اختصار
لنأخذ مزخرفاً بسيطاً يطبع رسالة قبل الاستدعاء وبعده:
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، والدالة الأصلية تعيش داخل الإغلاق، فالغلاف يتذكر المتغير func من الدالة الخارجية. تفصيل مهم: المزخرف يُطبَّق مرة واحدة، عند تنفيذ def، أي عند استيراد الوحدة، أما الغلاف فيعمل مع كل استدعاء.
الإغلاقات والمزخرفات
الإغلاق (closure) دالة متداخلة تتذكر متغيرات الدالة الخارجية بعد انتهائها. ولولا الإغلاقات لما وُجدت المزخرفات: بهذه الطريقة يصل wrapper إلى func. ويمكن أن يحفظ الإغلاق حالة، مثل عدّاد للاستدعاءات:
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
كيف تكتب مزخرفاً في Python: قالب عام
للمثال الأول عيبان: الغلاف لا يقبل وسائط، ويُخفي اسم الدالة الأصلية. والقالب العملي الذي يصلح أن تبدأ به أي مزخرف هو هذا:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# الكود قبل الاستدعاء
result = func(*args, **kwargs)
# الكود بعد الاستدعاء
return result
return wrapper
ثلاث قواعد في القالب:
*args, **kwargs— يقبل الغلاف أي وسائط ويمررها كما هي.return result— لا تنسَ إعادة النتيجة، وإلا ستعيد الدالة المزخرفةNoneدائماً.@functools.wraps(func)— ينقل بيانات الدالة الأصلية الوصفية إلى الغلاف.
مثال على مزخرف يقيس زمن التنفيذ:
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 يتغير اسم الدالة المزخرفة وتوثيقها وخصائصها الأخرى، فيظهر من الخارج الغلاف 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) يحصل الغلاف على __name__ و__doc__ و__module__ و__qualname__ و__annotations__ من الدالة الأصلية، ويحفظ في الخاصية __wrapped__ مرجعاً إلى الأصل. وهذا مهم للتصحيح والتتبع، وللأمر help()، وللأطر التي تسجّل الدوال بأسمائها، وللدالة inspect.signature التي تعرض التوقيع الحقيقي.
مزخرف بمعاملات
أحياناً يحتاج المزخرف إلى إعدادات مثل @retry(times=3) أو @app.route("/"). لذلك يُضاف مستوى إضافي من التداخل: دالة خارجية تستقبل المعاملات وتعيد المزخرف الحقيقي، ويُسمى هذا النمط مصنع المزخرفات.
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():
...
عدة مزخرفات على دالة واحدة
يمكن وضع المزخرفات فوق بعضها، وتُطبَّق من الأسفل إلى الأعلى، فالأقرب إلى def يُطبَّق أولاً.
@timer
@log_calls
def report():
...
# الشيء نفسه:
report = timer(log_calls(report))
عند الاستدعاء تعمل الطبقة الخارجية (timer) أولاً، فتقيس زمن عمل log_calls أيضاً. والترتيب مهم: مثلاً إذا كان أحد المزخرفات يتحقق من الصلاحيات والآخر يخزن النتيجة مؤقتاً.
المزخرفات المدمجة والقياسية في Python
توجد في اللغة والمكتبة القياسية مزخرفات كثيرة مفيدة:
| المزخرف | ماذا يفعل |
|---|---|
@property | يحوّل الطريقة إلى خاصية للقراءة فقط؛ ومع @x.setter تصبح خاصية قابلة للضبط |
@staticmethod | طريقة صنف بلا self، وهي دالة عادية في فضاء أسماء الصنف |
@classmethod | تستقبل الطريقة الصنف cls بدلاً من الكائن، وبهذا تُكتب المُنشئات البديلة |
@functools.cache، @functools.lru_cache | تخزّن النتائج حسب الوسائط |
@functools.wraps | تنسخ البيانات الوصفية للدالة إلى الغلاف |
@dataclasses.dataclass | تولّد __init__ و__repr__ و__eq__ للصنف |
@contextlib.contextmanager | تحوّل مولّداً إلى مدير سياق للعبارة with |
التخزين المؤقت: functools.cache وlru_cache
يحفظ المزخرف التخزيني نتيجة كل مجموعة وسائط، وعند الاستدعاء المتكرر يعيدها دون حساب. والمثال الكلاسيكي أرقام فيبوناتشي التكرارية، التي تُحسب دون تخزين في وقت أُسّي:
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'. ولا تخزّن إلا الدوال النقية، أي التي تعطي دائماً النتيجة نفسها لنفس الوسائط ولا تترك آثاراً جانبية.
ماذا يفعل المزخرف 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 تنشئ كائن الصنف الفرعي بشكل صحيح أيضاً.
مزخرفات الأصناف وأصناف المزخرفات
يمكن تطبيق المزخرف على صنف: يستقبل الصنف ويعيد الصنف نفسه أو معدَّلاً. وهكذا يعمل @dataclass. ومثال بسيط هو سجل للأصناف:
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
والوضع المعاكس أن يكون الصنف هو المزخرف. يصلح أي كائن فيه الطريقة __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
مزخرف للدوال غير المتزامنة
بالنسبة لدوال async def يجب أن يكون الغلاف أيضاً دالة مشتركة (coroutine) وينتظر النتيجة بـ 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
لماذا نستخدم المزخرفات في Python
- المنطق المشترك: التسجيل والمقاييس وقياس الزمن دون نسخ الكود في كل دالة.
- التخزين المؤقت:
@functools.cacheللحسابات النقية المكلفة. - التحقق: الصلاحيات والتحقق من الوسائط والتفويض في معالجات الويب.
- التسجيل: مسارات Flask وFastAPI (
@app.get("/")) وتركيبات pytest. - الموثوقية: إعادة المحاولة والمهلات الزمنية ومعالجة الأخطاء.
أهم فائدة هي فصل المسؤوليات: تقوم الدالة بعملها، ويُضاف السلوك الإضافي بسطر واحد، ويُزال بالسهولة نفسها.
أخطاء شائعة
- الغلاف لا يعيد النتيجة: الدالة بعد التزخرف تعيد
None. - غياب
*args, **kwargs:TypeErrorعند الاستدعاء بوسائط. - نسيان
wraps: اسم وتوثيق خاطئان، وتشويش في السجلات. - نسيان الأقواس في المصنع: كتابة
@repeatبدلاً من@repeat(3)تجعل الدالة تقع في المعاملtimes. - كود ثقيل داخل جسم المزخرف: يُنفَّذ عند الاستيراد لا عند الاستدعاء.
الخلاصة
المزخرفات في Python دوال عليا: تستقبل دالة وتعيد غلافاً، والرمز @ مجرد اختصار لعملية إسناد. ابدأ بالقالب الذي فيه *args, **kwargs وreturn result وfunctools.wraps، وللإعدادات أضف مستوى المصنع. والدوال والإغلاقات والتلميحات التي تقوم عليها المزخرفات تُشرح عملياً في دورة بايثون.
الأسئلة الشائعة
هل يمكن إزالة المزخرف من دالة؟
إذا استخدم المزخرف functools.wraps فالأصل متاح عبر الخاصية __wrapped__: تستدعي func.__wrapped__(...) الدالة دون الغلاف. وإلا فلا يبقى إلا حذف سطر @ من الكود.
متى يُنفَّذ كود المزخرف؟
جسم المزخرف نفسه يُنفَّذ مرة واحدة عند تعريف الدالة، عادةً أثناء استيراد الوحدة. أما الكود داخل wrapper فيُنفَّذ مع كل استدعاء للدالة المزخرفة.
هل يمكن تزخرف طرائق الصنف؟
نعم، يعمل المزخرف العادي ذو *args, **kwargs مع الطرائق أيضاً، إذ يصبح self أول عنصر في args.
ما الفرق بين المزخرف والوراثة؟
الوراثة تغيّر سلوك الصنف عبر التسلسل الهرمي، أما المزخرف فيغلّف دالة أو صنفاً محدداً من الخارج، ويتركب بسهولة مع مزخرفات أخرى دون تعديل الكود.
يُشرح هذا الموضوع بالتفصيل مع التطبيق العملي في دورة «Python 3.15: от новичка до профи».
الانتقال إلى الدورة