إذا رأيت في كود بايثون سطرًا يبدأ بالرمز @ مثل @login_required أو @staticmethod أو @property، فأنت تتعامل غالبًا مع Decorator. قد يبدو هذا المفهوم غامضًا في البداية، لكنه يصبح بسيطًا عندما تفهم أن الدوال في بايثون يمكن تمريرها إلى دوال أخرى وإرجاعها منها.
الـDecorator يسمح لك بإضافة سلوك قبل تنفيذ Function أو بعدها، أو تغيير طريقة استدعائها، من دون نسخ نفس الكود داخل كل Function. لهذا يُستخدم في التسجيل Logging، وقياس زمن التنفيذ، والتحقق من الصلاحيات، وإعادة المحاولة، والتخزين المؤقت، وغيرها من الأنماط المتكررة.
في الدرس 9 من سلسلة بايثون بعد الأساسيات سنبدأ من الفكرة التي تقف خلف Decorators، ثم نبني Decorator يدويًا، ونشرح معنى Wrapper، وكيف نجعلها تدعم *args و**kwargs، ولماذا نستخدم functools.wraps، وكيف نصنع Decorator تستقبل Arguments مثل @repeat(3)، وما الذي يحدث عند وضع أكثر من Decorator فوق Function واحدة.
{alertInfo}
الفكرة المختصرة: الـDecorator تستقبل callable، وتعيد callable أخرى غالبًا تكون Wrapper. والصيغة @decorator فوق Function هي طريقة مختصرة لتطبيق Decorator على تلك Function.
{getToc} $title={محتوى المقال}
ما هي Decorator في بايثون؟
Decorator هي callable تُستخدم لتحويل Function أو Class إلى نسخة أخرى يمكنها إضافة سلوك أو تعديل السلوك الحالي. في هذا الدرس سنركز على Function Decorators لأنها المدخل الأسهل لفهم الفكرة.
لنفرض أن لدينا Function عادية:
def hello():
print("Hello")
ونريد طباعة رسالة قبل تنفيذها ورسالة بعدها. يمكننا فعل ذلك داخل Function نفسها، لكن ماذا لو كان لدينا عشرون Function ونريد السلوك نفسه في جميعها؟ هنا تصبح Decorator مفيدة لأنها تجمع السلوك المتكرر في مكان واحد.
قبل Decorators: الدوال يمكن تخزينها في متغيرات
def hello():
print("Hello")
say_hello = hello
say_hello()
لاحظ أننا كتبنا hello بدون أقواس عند الإسناد. كتابة hello() تعني تنفيذ Function، أما hello فتعني تمرير Function نفسها كقيمة.
تمرير Function إلى Function أخرى
def hello():
print("Hello")
def run_function(func):
func()
run_function(hello)
الدالة run_function() استقبلت Function أخرى ثم نفذتها. هذه الفكرة من اللبنات الأساسية لفهم Decorators.
إرجاع Function من Function أخرى
def outer():
def inner():
print("Inside inner")
return inner
my_function = outer()
my_function()
هنا outer() أعادت Function داخلية، ولذلك أصبح my_function يشير إلى inner.
أول Decorator يدويًا بدون @
def logger(func):
def wrapper():
print("قبل التنفيذ")
func()
print("بعد التنفيذ")
return wrapper
ثم:
def hello():
print("Hello")
hello = logger(hello)
hello()
الناتج:
قبل التنفيذ
Hello
بعد التنفيذ
مررنا Function الأصلية إلى logger، وأنشأت logger Wrapper وأعادتها، ثم أصبح الاسم hello يشير إلى Function المغلفة.
استخدام الصيغة @decorator
بدل:
def hello():
print("Hello")
hello = logger(hello)
يمكننا كتابة:
@logger
def hello():
print("Hello")
الصيغتان تمثلان الفكرة نفسها: تمرير hello إلى logger ثم ربط الناتج باسم hello.
ما هي Wrapper؟
Wrapper هي Function داخلية تغلف Function الأصلية. تستطيع تنفيذ شيء قبل Function الأصلية، أو بعدها، أو فحص arguments، أو تغيير النتيجة قبل إعادتها.
def log_call(func):
def wrapper():
print(f"Calling {func.__name__}")
return func()
return wrapper
لماذا return func() مهمة؟
من الأخطاء الشائعة أن تنفذ Function الأصلية داخل Wrapper لكن لا تعيد نتيجتها.
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def add():
return 10 + 5
print(add())
ستظهر None لأن Wrapper لم تُرجع نتيجة Function الأصلية.
التصحيح:
def decorator(func):
def wrapper():
return func()
return wrapper
ماذا لو كانت Function تستقبل Arguments؟
إذا كانت Wrapper معرفة بدون parameters ثم زينت دالة مثل add(a, b)، ستظهر مشكلة عند تمرير القيم.
@log_call
def add(a, b):
return a + b
print(add(3, 4))
الحل باستخدام *args و**kwargs
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(
*args,
**kwargs
)
return wrapper
الآن تعمل Decorator مع positional arguments ومع keyword arguments.
@log_call
def greet(name, greeting="Hello"):
return f"{greeting}, {name}"
print(
greet(
"Ali",
greeting="Hi"
)
)
ماذا تفعل *args؟
*args تجمع positional arguments في Tuple:
def show_args(*args):
print(args)
show_args(10, 20, 30)
ماذا تفعل **kwargs؟
**kwargs تجمع keyword arguments في Dictionary:
def show_kwargs(**kwargs):
print(kwargs)
show_kwargs(
name="Ali",
city="Aden"
)
لماذا نستخدم args وkwargs في Decorators؟
لأن Decorator العامة قد تستخدم مع Functions بتواقيع مختلفة. هذا النمط يسمح بتمرير arguments من Wrapper إلى Function الأصلية بدون معرفة أسمائها مسبقًا:
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
مشكلة metadata: لماذا يصبح __name__ هو wrapper؟
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def hello():
"""Say hello."""
print("Hello")
print(hello.__name__)
قد يظهر wrapper بدل hello، لأن الاسم أصبح يشير إلى Wrapper.
الحل باستخدام functools.wraps
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
ثم:
@decorator
def hello():
"""Say hello."""
print("Hello")
print(hello.__name__)
print(hello.__doc__)
functools.wraps تساعد Wrapper على الاحتفاظ بمعلومات Function الأصلية مثل الاسم وdocstring والـmetadata المرتبطة بها.
قالب عملي لكتابة Function Decorator
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# منطق قبل التنفيذ
result = func(
*args,
**kwargs
)
# منطق بعد التنفيذ
return result
return wrapper
هذا ليس قالبًا إلزاميًا لكل Decorator، لكنه نقطة بداية جيدة: يدعم arguments، ويحافظ على return، ويستخدم wraps.
مثال عملي: Decorator لقياس زمن التنفيذ
from functools import wraps
from time import perf_counter
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = perf_counter()
result = func(
*args,
**kwargs
)
elapsed = perf_counter() - start
print(
f"{func.__name__}: "
f"{elapsed:.6f} seconds"
)
return result
return wrapper
الاستخدام:
@timer
def calculate():
total = 0
for number in range(1_000_000):
total += number
return total
print(calculate())
استخدمنا perf_counter() لقياس المدة المنقضية، وجعلنا كود القياس منفصلًا عن منطق calculate().
مثال عملي: Logging Decorator
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[CALL] {func.__name__}")
result = func(
*args,
**kwargs
)
print(f"[DONE] {func.__name__}")
return result
return wrapper
ثم:
@log_call
def save_user(name):
print(f"Saving {name}")
save_user("Ali")
في مشروع فعلي من الأفضل استخدام module logging بدل الاعتماد على print() فقط، لكن المثال يوضح فكرة Decorator.
Decorator تستقبل Arguments
حتى الآن كتبنا @timer. لكن ماذا لو أردنا:
@repeat(3)
def hello():
print("Hello")
هنا repeat(3) تُستدعى أولًا، لذلك يجب أن تعيد Decorator تستقبل Function الأصلية.
بناء @repeat(3)
from functools import wraps
def repeat(count):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(count):
result = func(
*args,
**kwargs
)
return result
return wrapper
return decorator
الاستخدام:
@repeat(3)
def hello():
print("Hello")
hello()
الناتج:
Hello
Hello
Hello
كيف نقرأ Decorator التي تستقبل Arguments؟
repeat(count)
↓
decorator(func)
↓
wrapper(*args, **kwargs)
الطبقة الأولى تستقبل إعدادات Decorator، والثانية تستقبل Function الأصلية، والثالثة تستقبل arguments الخاصة بالدالة وقت التشغيل.
ما علاقة Decorators بالـClosures؟
في مثال repeat تستطيع Wrapper استخدام count رغم أن repeat() انتهى تنفيذها. هذا مثال على Closure: Function داخلية تحتفظ بإمكانية الوصول إلى متغيرات من النطاق الخارجي.
Decorator مع أكثر من إعداد
from functools import wraps
def repeat(count, message):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(message)
for _ in range(count):
func(*args, **kwargs)
return wrapper
return decorator
ثم:
@repeat(
count=2,
message="Starting"
)
def hello():
print("Hello")
hello()
ماذا لو وضعنا أكثر من Decorator؟
@decorator_a
@decorator_b
def task():
pass
يمكن فهمها تقريبًا كالتالي:
task = decorator_a(
decorator_b(task)
)
أي أن Decorator الأقرب إلى Function تُطبق أولًا، ثم تُغلف النتيجة بالتي فوقها.
مثال يوضح ترتيب Decorators
from functools import wraps
def first(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("first before")
result = func(*args, **kwargs)
print("first after")
return result
return wrapper
def second(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("second before")
result = func(*args, **kwargs)
print("second after")
return result
return wrapper
ثم:
@first
@second
def task():
print("task")
task()
الناتج:
first before
second before
task
second after
first after
لماذا ترتيب Decorators مهم؟
لأن كل Decorator تستقبل ناتج التي تحتها، لذلك تغيير الترتيب قد يغير السلوك. هذه النقطة مهمة في أطر العمل التي تستخدم Decorators للتحقق أو التحويل أو التسجيل.
Decorators مدمجة رأيناها من قبل
@staticmethod@classmethod@property
إذا كنت قرأت درس classmethod وstaticmethod، فأنت استخدمت Decorators بالفعل. الفرق أن هذا الدرس يشرح الآلية العامة وراء الصيغة @....
أخطاء شائعة عند كتابة Decorators
1. نسيان return wrapper
def decorator(func):
def wrapper():
return func()
# نسينا return wrapper
الصحيح:
def decorator(func):
def wrapper():
return func()
return wrapper
2. نسيان return func(...)
إذا كانت Function الأصلية تعيد قيمة ولم تعدها Wrapper، ستضيع النتيجة.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
3. Wrapper لا تستقبل Arguments
استخدم *args و**kwargs في Decorator العامة إذا كانت ستغلف Functions مختلفة.
4. عدم استخدام functools.wraps
قد تعمل Decorator، لكن اسم Function وdocstring والـmetadata ستبدو كبيانات Wrapper. لذلك استخدم:
@wraps(func)
def wrapper(*args, **kwargs):
...
5. تنفيذ Function في وقت غير مقصود
func تعني Function نفسها، بينما func() تعني تنفيذها. وضع الأقواس في مكان غير صحيح قد يجعل التنفيذ يحدث عند تطبيق Decorator بدل وقت استدعاء Function المغلفة.
6. الخلط بين Arguments الخاصة بالـDecorator والدالة
في:
@repeat(3)
def greet(name):
print(name)
القيمة 3 تخص repeat، أما name فتصل لاحقًا إلى Wrapper عند استدعاء greet().
Decorator لعد مرات الاستدعاء
from functools import wraps
def count_calls(func):
count = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"Call number: {count}")
return func(*args, **kwargs)
return wrapper
ثم:
@count_calls
def hello():
print("Hello")
hello()
hello()
hello()
استخدمنا nonlocal لأن count موجودة في النطاق الخارجي ونريد تعديلها من داخل Wrapper.
Decorator مع Method داخل Class
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Product:
def __init__(self, name):
self.name = name
@log_call
def show_name(self):
print(self.name)
ثم:
product = Product("Keyboard")
product.show_name()
self تمر ضمن *args إلى Wrapper ثم إلى Method الأصلية.
لماذا @staticmethod و@classmethod Decorators؟
لأنهما تغيران طريقة ربط Function المعرفة داخل Class. @staticmethod تمنع تمرير instance أو class تلقائيًا، بينما @classmethod تجعل class تُمرر كأول argument.
الوصول إلى Function الأصلية عبر __wrapped__
عند استخدام @wraps(func) يتم توفير __wrapped__ عادةً للإشارة إلى Function الأصلية:
@log_call
def hello():
print("Hello")
print(hello.__wrapped__)
هذه الخاصية مفيدة لأدوات الفحص والاختبارات وبعض عمليات introspection.
هل Decorators تؤثر على Type Hints؟
Decorator البسيطة قد تجعل أدوات الفحص ترى signature الخاصة بـWrapper بدل Function الأصلية. @wraps(func) يساعد في metadata، لكن الحفاظ على type signature بصورة دقيقة في Decorators العامة موضوع أعمق يمكن معالجته لاحقًا باستخدام أدوات typing مثل ParamSpec وTypeVar.
متى تكون Decorator فكرة جيدة؟
- عندما تريد Logging متكررًا حول عدة Functions.
- لقياس زمن التنفيذ بنفس الطريقة.
- للتحقق من شرط قبل التنفيذ.
- لإضافة Retry مضبوط لنوع خطأ محدد.
- للتخزين المؤقت أو Registration في بعض الأطر.
- عندما تقلل Decorator التكرار وتجعل المسؤولية أوضح.
متى لا أستخدم Decorator؟
لا تستخدمها لمجرد أن الصيغة تبدو متقدمة. إذا كان السلوك يستخدم مرة واحدة، أو إذا كانت Decorator تخفي تدفقًا مهمًا وتجعل تتبع البرنامج أصعب، فقد تكون Function عادية أو استدعاء واضح أفضل.
Decorator أم Function مساعدة؟
| الحالة | الاختيار المناسب غالبًا |
|---|---|
| سلوك يحيط بعدة Functions بالطريقة نفسها | Decorator. |
| عملية مستقلة تستدعيها في مكان واضح | Function مساعدة. |
| منطق يحتاج تخصيصًا كبيرًا لكل Function | قد تكون Decorator أقل وضوحًا. |
| سلوك متكرر قبل/بعد التنفيذ | Decorator مناسبة. |
مثال كامل يجمع أهم المفاهيم
from functools import wraps
from time import perf_counter
def log_and_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[START] {func.__name__}")
start = perf_counter()
result = func(*args, **kwargs)
elapsed = perf_counter() - start
print(f"[END] {func.__name__}")
print(f"Time: {elapsed:.6f}s")
return result
return wrapper
@log_and_time
def total(numbers):
return sum(numbers)
result = total(range(100_000))
print("Result:", result)
هذا المثال يجمع Function تُمرر إلى Decorator، وWrapper، و*args و**kwargs، و@wraps(func)، ومنطقًا قبل التنفيذ وبعده، والحفاظ على قيمة return.
تمرين 1: Decorator تطبع اسم Function
أنشئ show_function_name تطبع اسم Function قبل تنفيذها.
حل مقترح
from functools import wraps
def show_function_name(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(func.__name__)
return func(*args, **kwargs)
return wrapper
تمرين 2: Decorator repeat
اجعل @repeat(2) تكرر Function مرتين مع دعم arguments.
حل مقترح
from functools import wraps
def repeat(count):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(count):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
تمرين 3: Decorator تمنع النص الفارغ
from functools import wraps
def require_text(func):
@wraps(func)
def wrapper(text, *args, **kwargs):
if not text.strip():
raise ValueError(
"النص لا يمكن أن يكون فارغًا"
)
return func(
text,
*args,
**kwargs
)
return wrapper
قائمة فحص لفهم Decorators
- هل تعرف أن Function يمكن تمريرها كقيمة؟
- هل تفهم أن Decorator تستقبل Function وتعيد callable؟
- هل تعرف أن
@decoratorتقابل تقريبًاfunc = decorator(func)؟ - هل تعرف وظيفة Wrapper؟
- هل تستطيع تمرير
*argsو**kwargs؟ - هل تحافظ على قيمة return؟
- هل تستخدم
@wraps(func)عندما تكون metadata مهمة؟ - هل تفهم لماذا Decorator ذات Arguments تحتاج طبقة إضافية؟
- هل تستطيع قراءة ترتيب أكثر من Decorator فوق Function؟
روابط داخلية مفيدة من بايثون العرب
- مسار بايثون بعد الأساسيات
- كورس أساسيات بايثون
- أساسيات بايثون 38: Encapsulation وpublic وprotected وprivate
مصادر رسمية للتوسع
- تعريف Decorator في توثيق بايثون الرسمي
- Function Definitions وDecorator syntax في مرجع بايثون
- توثيق functools.wraps الرسمي
- توثيق classmethod الرسمي
- توثيق staticmethod الرسمي
الخلاصة
Decorators في بايثون ليست سحرًا. الفكرة الأساسية أن Function يمكن تمريرها إلى Function أخرى، وأن Decorator تستقبل Function وتعيد callable جديدة غالبًا تكون Wrapper. لذلك فإن @decorator هي صيغة مختصرة لربط Function بنتيجة decorator(function).
عند كتابة Decorator عامة، اجعل Wrapper تدعم *args و**kwargs إذا احتجت إلى العمل مع تواقيع مختلفة، وأعد نتيجة Function الأصلية حتى لا تضيع قيمة return، واستخدم functools.wraps للحفاظ على metadata المهمة.
أما Decorators التي تستقبل Arguments مثل @repeat(3) فتحتاج طبقة إضافية: Function خارجية تستقبل الإعدادات، ثم تعيد Decorator تستقبل Function، ثم تعيد Wrapper. وعند وضع عدة Decorators فوق Function واحدة يصبح ترتيب التغليف مهمًا.
{alertSuccess} القاعدة المهمة: ابدأ دائمًا بفهمfunction = decorator(function). عندما تصبح هذه الجملة واضحة، تتحول صيغة@decoratorمن شيء غامض إلى اختصار منظم لإعادة تغليف Function.
أسئلة شائعة
ما هي Decorator في بايثون؟
هي callable تستقبل Function أو Class وتعيد قيمة تُستخدم بدلها، وغالبًا تستخدم لإضافة سلوك متكرر حول Function دون تعديل جسمها مباشرة.
ماذا تعني @decorator في بايثون؟
يمكن فهمها تقريبًا كأن بايثون تنفذ function = decorator(function) بعد إنشاء Function.
ما هي wrapper في Decorators؟
هي Function داخلية تعيدها Decorator عادةً، وتستدعي Function الأصلية مع إضافة منطق قبلها أو بعدها أو حولها.
كيف أجعل Decorator تعمل مع Arguments؟
في Decorator العامة، عرّف Wrapper بـ*args و**kwargs ثم مررهما إلى Function الأصلية باستخدام func(*args, **kwargs).
لماذا نستخدم functools.wraps؟
للمساعدة على الاحتفاظ بمعلومات Function الأصلية مثل __name__ وdocstring والـmetadata بعد تغليفها.
لماذا يتحول __name__ إلى wrapper؟
لأن الاسم الأصلي أصبح يشير إلى Wrapper التي أعادتها Decorator. استخدام @wraps(func) يساعد على الحفاظ على metadata الأصلية.
كيف أكتب Decorator تستقبل parameter مثل @repeat(3)؟
استخدم ثلاث طبقات عادةً: Function خارجية تستقبل الإعداد مثل count، ثم Decorator تستقبل Function، ثم Wrapper تستقبل Arguments الخاصة بالدالة.
ما الفرق بين Arguments الخاصة بالـDecorator وArguments الخاصة بالدالة؟
في @repeat(3) القيمة 3 تُستخدم عند إنشاء Decorator، أما Arguments الخاصة بالدالة فتمر لاحقًا عند استدعاء Function المغلفة.
كيف أستخدم أكثر من Decorator على Function واحدة؟
ضعها في أسطر متتالية فوق Function. ويمكن فهم @a فوق @b تقريبًا كـfunc = a(b(func)).
هل ترتيب Decorators مهم؟
نعم. تغيير ترتيبها قد يغير السلوك لأن كل Decorator تغلف القيمة التي أعادتها Decorator الموجودة تحتها.
هل @staticmethod و@classmethod Decorators؟
نعم، وهما Decorators مدمجتان تغيران طريقة ربط Functions المعرفة داخل Class.
هل @property Decorator أيضًا؟
نعم. تستخدم @property لتحويل Method إلى واجهة يمكن الوصول إليها بصيغة Attribute مع التحكم في القراءة والكتابة.
متى أستخدم Decorator؟
عندما يكون لديك سلوك متكرر يحيط بعدة Functions مثل Logging أو قياس الزمن أو التحقق من شرط، وكانت Decorator تجعل القصد أوضح وتقلل التكرار.
هل يجب استخدام Decorators في كل مشروع؟
لا. استخدمها عندما تحسن التصميم. إذا كانت تخفي تدفقًا مهمًا أو تستخدم مرة واحدة بلا فائدة واضحة، فقد يكون الاستدعاء المباشر أبسط.
كيف أحافظ على return للدالة الأصلية؟
خزن نتيجة func(*args, **kwargs) ثم أعدها، أو اكتب return func(*args, **kwargs) مباشرة إذا لم تحتج إلى منطق بعدها.
ما علاقة Decorators بالـClosures؟
تستخدم Decorators غالبًا Closures لأن Wrapper تحتاج إلى الاحتفاظ بالوصول إلى Function الأصلية وإلى إعدادات من نطاق خارجي مثل count في @repeat(3).



