Menu
العربية
جرّب في Playground

الديكوريتورز في بايثون: @ وshargumentsوwraps

تعرّف على مفهوم الـ Decorators في بايثون، كيف تكتب ديكوريتور خاص بك، وأهم الأنماط: التمرير بالوسائط، التكديس، واستخدام functools.wraps.

الـ decorator ببساطة: دالة تغلّف دالة أخرى

العبارة تبدو مجرّدة نوعاً ما، لكن الفكرة من الداخل بسيطة جداً. الـ decorator في بايثون يستقبل دالة ويُرجع دالة. الدالة المُرجَعة عادةً ما تستدعي الدالة الأصلية مع إضافة سلوك جديد حولها.

وهذا أبسط مثال ممكن:

main.py
Output
Click Run to see the output here.

shout هو الـ decorator. ياخذ دالة (greet)، ويبني دالة جديدة (wrapper) تستدعي الأصلية وتحوّل ناتجها إلى أحرف كبيرة، ثم يرجّعها. وإعادة الإسناد greet = shout(greet) تستبدل الدالة الأصلية بالنسخة المغلّفة.

هذا النمط من إعادة الإسناد شائع جدًا، لدرجة أن بايثون خصّصت له صيغة مستقلة.

الرمز @ مجرد اختصار لإعادة الإسناد

كتابة @name في السطر الذي يسبق def تكافئ تمامًا name = name(...) مباشرة بعد تعريف الدالة:

main.py
Output
Click Run to see the output here.

@shout تعني ببساطة "طبّق الـ decorator المسمّى shout على هذه الدالة". بايثون ينفّذ greet = shout(greet) مباشرة بعد تعريف الدالة بـ def — نفس الآلية التي رأيناها قبل قليل، لكن بكتابة أقل.

كلما رأيت @name، استبدله ذهنيًا بـ function = name(function). هذا كل ما يعنيه هذا الصياغة.

التعامل مع وسائط الدوال

معظم الدوال تستقبل وسائط (arguments)، وأي decorator عملي يجب أن يُمرّرها كما هي. الصيغة المتعارف عليها بين مطوّري بايثون هي *args, **kwargs — وهي طريقة بايثون لاستقبال أي عدد من الوسائط بأي شكل — لأن الـ wrapper لا يجب أن يهتم بما تتوقعه الدالة المُغلَّفة:

main.py
Output
Click Run to see the output here.

*args يلتقط كل الوسائط الموضعية (positional arguments)، و**kwargs يلتقط كل الوسائط المُسمّاة (keyword arguments). الـ wrapper يُمرّر كل شيء للدالة الأصلية كما هو دون أي تعديل، ثم ينفّذ المهمة الإضافية التي أُنشئ الديكوريتور من أجلها — وفي مثالنا هنا تحويل النتيجة إلى أحرف كبيرة.

هذه هي الصيغة التي تأخذها معظم الـ decorators في بايثون فعليًا.

مثال عملي أكثر فائدة: قياس زمن التنفيذ

خلينا نكتب ديكوريتور يطبع الوقت الذي تستغرقه الدالة:

main.py
Output
Click Run to see the output here.

النمط بسيط: نفّذ شيئًا قبل استدعاء الدالة، ثم شيئًا بعده. هذا ما تفعله معظم الـ decorators في النهاية. تسجيل اللوجات، التحقق من الصلاحيات، إعادة المحاولة، والتحقق من المدخلات، كلها تتبع نفس الشكل.

الحفاظ على هوية الدالة الأصلية باستخدام functools.wraps

عندما تطبّق decorator على دالة، فأنت عمليًا تستبدلها بدالة أخرى، وهذا يعني أن الدالة المغلّفة تفقد قيم __name__ و__doc__ الأصلية الخاصة بها:

main.py
Output
Click Run to see the output here.

greet.__name__ صار "wrapper" والـ docstring اختفى. هذه مشكلة تكسر help() والـ tracebacks وأي أداة تعتمد على فحص الدوال.

الحل سطر واحد: ضع @functools.wraps(func) فوق الدالة الداخلية، وهي بتنقل الميتاداتا كاملة.

main.py
Output
Click Run to see the output here.

دايمًا أضِف @wraps(func) للدالة الداخلية. مش هتكلفك حاجة، وهتوفر عليك جلسات debugging محيّرة في المستقبل.

decorator بوسائط في بايثون

أحيانًا الديكوريتور نفسه محتاج إعدادات — زي "أعِد تنفيذ الدالة دي 3 مرات" أو "سجّل اللوج على مستوى DEBUG". ده معناه طبقة تداخل إضافية: دالة خارجية بتستقبل الوسائط وبترجّع ديكوريتور.

main.py
Output
Click Run to see the output here.

ثلاث طبقات قد تبدو كثيرة، لكن اقرأها من الخارج للداخل:

  1. repeat(times=3) هو استدعاء للدالة، ويُرجِع decorator.
  2. decorator هو الـ decorator الفعلي — يأخذ دالة ويُرجِع نسخة مغلَّفة منها.
  3. wrapper هي الدالة المغلَّفة التي تعمل فعليًا وقت الاستدعاء.

هذا القالب نفسه هو ما يجعل أمثال @retry(times=5) و@cache(maxsize=100) وحتى ديكوريتورز إطارات العمل مثل @app.route("/users") ممكنة. بمجرد أن تستوعب نمط الطبقات الثلاث، ستجد كل أفراد هذه العائلة مفهومة بالطريقة نفسها.

تكديس الديكوريتورز في بايثون

تقدر تطبّق أكثر من decorator على نفس الدالة، وهي تتراكم من الأسفل إلى الأعلى — أي أن الأقرب إلى def هو الذي يُنفَّذ أولًا:

main.py
Output
Click Run to see the output here.

الديكوريتور add_exclaim يُطبَّق أولاً ويضيف علامة التعجب !، ثم يأتي shout بعده ليحوّل كل شيء إلى أحرف كبيرة، فتكون النتيجة النهائية HI, ROSA!.

الترتيب هنا مهم جداً. لو عكست ترتيب التكديس، ستحصل على HI, ROSA! لكن مع إضافة علامة التعجب بعد التحويل لأحرف كبيرة — الفرق غير ملحوظ في هذا المثال البسيط، لكن تخيّل ديكوريتور يُنسّق JSON: تشغيله قبل أو بعد ديكوريتور يُسجّل المدخلات قد يُعطيك نتائج مختلفة تماماً.

ديكوريتورز جاهزة ستصادفها كثيراً

تأتي بايثون ومكتبتها القياسية مع مجموعة من الديكوريتورز الجاهزة التي ستقابلها في الكود الحقيقي:

main.py
Output
Click Run to see the output here.
  • @property يحوّل الدالة إلى سمة محسوبة (attribute) يمكن قراءتها مباشرة.
  • @staticmethod يُستخدم لتمييز دالة لا تعتمد على self ولا على cls.
  • @classmethod يستقبل الكلاس نفسه كوسيط باسم cls بدل الكائن — مفيد جدًا لإنشاء "مُنشئات بديلة" (alternative constructors).
  • @functools.lru_cache يخزّن نتائج الدالة في الذاكرة، فأي استدعاء لاحق بنفس الوسائط يُعاد من الكاش مباشرة.

ديكوريتورز أطر العمل مثل @app.route و@pytest.fixture و@dataclass تعتمد على نفس الآلية تمامًا، لا شيء سحري فيها — مجرد دوال تغلّف دوالًا أخرى.

متى تكتب decorator ومتى تتجنّبه؟

اكتب decorator عندما تريد تطبيق سلوك واحد على عدة دوال: قياس الزمن، التسجيل (logging)، إعادة المحاولة، التحقق من الصلاحيات. الفكرة الأساسية هي إبقاء هذا السلوك خارج جسم الدالة الأصلية.

وتجنّبه في الحالات التالية:

  • إذا كان السلوك خاصًا بدالة واحدة بعينها. ضعه داخلها مباشرة.
  • إذا كنت تحتاجه لأغراض الاختبار فقط. استخدام fixture أو تمرير وسيط أوضح وأفضل.
  • إذا وجدت نفسك تكدّس أربعة أو خمسة ديكوريتورز فوق بعض. عند هذه النقطة، يصبح مسار التنفيذ مخبّأً داخل سلسلة الديكوريتورز، ويضطر القارئ لفكّ كل طبقة ليفهم ما يجري فعلًا. في هذه الحالة، دالة مساعدة بسيطة تكون أوضح بكثير.

الديكوريتورز أداة حادّة. إذا استُخدمت بذكاء أبقت الكود نظيفًا (DRY) والنيّة واضحة، وإذا أُسيء استخدامها أخفت ما يقوم به البرنامج فعلًا. حين تتردّد في استخدامها، انحَز دائمًا إلى الخيار "الأوضح".

التالي: Type Hints

الديكوريتورز من أكثر الأماكن التي ستصادف فيها تلميحات الأنواع (type hints)، لأن دوال wrapper عادةً ما تُضيف تعليقات توضيحية على توقيعها. تلميحات الأنواع ميزة صغيرة لكنها تؤتي ثمارها سريعًا، وهي موضوعنا القادم.

الأسئلة الشائعة

ما هو الـ decorator في بايثون؟

الـ decorator باختصار هو دالة تأخذ دالة أخرى وتُرجع دالة جديدة، غالبًا ما تكون نسخة مُغلّفة من الأصلية مع سلوك إضافي. تستخدمه بكتابة @decorator_name فوق تعريف الدالة مباشرة، وهذه الصيغة مجرد اختصار لكتابة func = decorator_name(func).

فيمَ تُستخدم الديكوريتورز في بايثون؟

تُستخدم لإضافة سلوك حول الدالة دون تعديل جسمها: تسجيل الأحداث (logging)، قياس زمن التنفيذ، التخزين المؤقت (caching)، التحقق من الصلاحيات، التحقق من المدخلات، وإعادة المحاولة. وفريمووركات كثيرة تعتمد عليها بشكل أساسي مثل @app.route(...) في Flask، و@pytest.fixture في pytest، بالإضافة إلى @property و@staticmethod المدمجين في اللغة.

هل أستطيع كتابة ديكوريتور خاص بي؟

نعم، فالديكوريتور ما هو إلا دالة تستقبل دالة وتُرجع دالة. في الغالب ستُعرّف دالة داخلية صغيرة تُغلّف الاستدعاء الأصلي وتُنفّذ شيئًا قبله أو بعده أو حوله. ويُفضّل استخدام functools.wraps على الدالة الداخلية للحفاظ على اسم الدالة الأصلية وعلى الـ docstring الخاص بها.

تعلّم البرمجة مع Coddy

ابدأ الآن