الديكورات (Decorators): إضافة ميزات جديدة للدوال دون تعديل الكود الأصلي


الديكورات (Decorators): إضافة ميزات جديدة للدوال دون تعديل الكود الأصلي

ماذا سنتعلم اليوم؟

سنتعلم كيفية استخدام الديكورات في بايثون لتوسيع وظائف الدوال الموجودة دون الحاجة لتعديل شيفرتها الأصلية، مما يعزز قابلية الصيانة وإعادة الاستخدام.

1. فهم الدوال كمواطنين من الدرجة الأولى (First-Class Citizens)

في بايثون، تعتبر الدوال "مواطنين من الدرجة الأولى"، مما يعني أنه يمكن معاملتها كأي كائن آخر: يمكن تمريرها كوسائط لدوال أخرى، تخزينها في متغيرات، وإعادتها كقيم من دوال أخرى. هذا المفهوم هو الأساس لفهم كيفية عمل الديكورات.

دعونا نرى مثالاً بسيطاً:

def greet(name):
    # دالة بسيطة تقوم بالترحيب باسم معين
    return f"أهلاً بك يا {name}!"

def call_function(func, *args, **kwargs):
    # دالة تستقبل دالة أخرى كوسيط وتقوم بتنفيذها
    print("سأقوم بتنفيذ الدالة التي تم تمريرها...")
    return func(*args, **kwargs)

# تمرير الدالة greet كـ argument إلى call_function
result = call_function(greet, "علي")
print(result)

# يمكن أيضاً إسناد دالة لمتغير
my_greeting_func = greet
print(my_greeting_func("فاطمة"))

2. بناء ديكور بسيط (Simple Decorator)

الديكور هو دالة تأخذ دالة أخرى كوسيط، وتضيف إليها بعض الوظائف، ثم تعيد دالة جديدة (عادةً دالة داخلية تسمى "wrapper"). الهدف هو تعديل سلوك الدالة دون تعديل كودها الأصلي.

لنقم ببناء ديكور يقوم بتسجيل (logging) وقت تنفيذ الدالة:

import time

def timer_decorator(func):
    # الديكور يستقبل دالة (func) كوسيط
    def wrapper(*args, **kwargs):
        # دالة الـ wrapper هي الدالة الجديدة التي ستحل محل الدالة الأصلية
        start_time = time.time() # تسجيل وقت البدء
        result = func(*args, **kwargs) # تنفيذ الدالة الأصلية
        end_time = time.time() # تسجيل وقت الانتهاء
        execution_time = end_time - start_time
        print(f"الدالة '{func.__name__}' استغرقت {execution_time:.4f} ثانية للتنفيذ.")
        return result # إعادة نتيجة الدالة الأصلية
    return wrapper # الديكور يعيد دالة الـ wrapper

3. تطبيق الديكور على الدوال باستخدام @

بايثون توفر صيغة مختصرة وجميلة لتطبيق الديكورات باستخدام الرمز @. بدلاً من كتابة my_function = decorator(my_function)، يمكننا ببساطة وضع @decorator فوق تعريف الدالة.

دعونا نطبق الديكور timer_decorator على دالة تقوم بعملية حسابية تستغرق وقتاً:

# ... (الجزء السابق من الكود، بما في ذلك تعريف timer_decorator) ...
import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"الدالة '{func.__name__}' استغرقت {execution_time:.4f} ثانية للتنفيذ.")
        return result
    return wrapper

@timer_decorator # تطبيق الديكور timer_decorator على الدالة التالية
def calculate_sum(n):
    # دالة تقوم بحساب مجموع الأرقام حتى n
    print(f"بدء حساب مجموع الأرقام حتى {n}...")
    total = 0
    for i in range(n):
        total += i
    time.sleep(0.1) # محاكاة عمل يستغرق وقتاً
    print(f"انتهى حساب المجموع.")
    return total

# استدعاء الدالة المزينة. سيتم تشغيل الديكور تلقائياً.
result_sum = calculate_sum(1000000)
print(f"المجموع المحسوب: {result_sum}")

4. تمرير الوسائط إلى الدوال المزينة (Decorated Functions with Arguments)

الديكورات التي كتبناها حتى الآن قادرة على التعامل مع الدوال التي تقبل أي عدد من الوسائط (*args) والوسائط المسماة (**kwargs) بفضل استخدامها في دالة الـ wrapper. هذا يجعلها مرنة للغاية.

ملاحظة تقنية: استخدام functools.wraps@
عند استخدام الديكورات، تفقد الدالة المزينة بعض بياناتها الوصفية الأصلية مثل اسمها (__name__) وسلسلة التوثيق (__doc__)، لأن الديكور يعيد دالة الـ wrapper بدلاً منها. للحفاظ على هذه البيانات الوصفية، نستخدم @functools.wraps(func) على دالة الـ wrapper داخل الديكور. هذا يجعل الدالة المزينة تبدو وكأنها الدالة الأصلية للمفتشين والـ debuggers.

لنعدل الديكور timer_decorator ليشمل functools.wraps ولنظهر مرونته مع وسائط الدالة:

import time
import functools # استيراد functools

def timer_decorator(func):
    @functools.wraps(func) # استخدام wraps للحفاظ على بيانات الدالة الأصلية
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print(f"بدء تنفيذ الدالة '{func.__name__}'...")
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"الدالة '{func.__name__}' استغرقت {execution_time:.4f} ثانية للتنفيذ.")
        return result
    return wrapper

@timer_decorator
def power(base, exponent):
    # دالة تقوم برفع رقم لأس معين
    print(f"حساب {base} مرفوعة للأس {exponent}...")
    time.sleep(0.05) # محاكاة عمل يستغرق وقتاً
    return base ** exponent

@timer_decorator
def greet_person(name, greeting="مرحباً"):
    # دالة ترحيب تقبل اسماً وتحية اختيارية
    print(f"تحضير التحية لـ {name}...")
    time.sleep(0.02)
    return f"{greeting}, {name}!"

# استدعاء الدوال المزينة بوسائط مختلفة
print(f"\nالنتيجة: {power(2, 10)}")
print(f"\nالنتيجة: {greet_person('سارة')}")
print(f"النتيجة: {greet_person('أحمد', greeting='صباح الخير')}")

# لنتحقق من اسم الدالة الأصلية بعد التزيين (بفضل functools.wraps)
print(f"\nاسم الدالة power بعد التزيين: {power.__name__}")
print(f"سلسلة توثيق الدالة power: {power.__doc__}")

الكود النهائي الكامل

import time
import functools

# 1. فهم الدوال كمواطنين من الدرجة الأولى (First-Class Citizens)
def greet(name):
    # دالة بسيطة تقوم بالترحيب باسم معين
    return f"أهلاً بك يا {name}!"

def call_function(func, *args, **kwargs):
    # دالة تستقبل دالة أخرى كوسيط وتقوم بتنفيذها
    print("سأقوم بتنفيذ الدالة التي تم تمريرها...")
    return func(*args, **kwargs)

print("--- أمثلة على الدوال كمواطنين من الدرجة الأولى ---")
result_first_class = call_function(greet, "علي")
print(result_first_class)
my_greeting_func = greet
print(my_greeting_func("فاطمة"))
print("-" * 50)

# 2. بناء وتطبيق ديكور بسيط مع functools.wraps
def timer_decorator(func):
    # استخدام wraps للحفاظ على بيانات الدالة الأصلية مثل __name__ و __doc__
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        print(f"بدء تنفيذ الدالة '{func.__name__}' المزينة...")
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"الدالة '{func.__name__}' استغرقت {execution_time:.4f} ثانية للتنفيذ.")
        return result
    return wrapper

@timer_decorator
def calculate_sum(n):
    """دالة تحسب مجموع الأرقام حتى n."""
    print(f"بدء حساب مجموع الأرقام حتى {n} داخل الدالة الأصلية...")
    total = 0
    for i in range(n):
        total += i
    time.sleep(0.1) # محاكاة عمل يستغرق وقتاً
    print(f"انتهى حساب المجموع داخل الدالة الأصلية.")
    return total

@timer_decorator
def power(base, exponent):
    """دالة تقوم برفع رقم لأس معين."""
    print(f"حساب {base} مرفوعة للأس {exponent} داخل الدالة الأصلية...")
    time.sleep(0.05) # محاكاة عمل يستغرق وقتاً
    return base ** exponent

@timer_decorator
def greet_person(name, greeting="مرحباً"):
    """دالة ترحيب تقبل اسماً وتحية اختيارية."""
    print(f"تحضير التحية لـ {name} داخل الدالة الأصلية...")
    time.sleep(0.02)
    return f"{greeting}, {name}!"

print("\n--- أمثلة على الدوال المزينة ---")
result_sum = calculate_sum(1000000)
print(f"المجموع المحسوب: {result_sum}")

result_power = power(2, 10)
print(f"الأس المحسوب: {result_power}")

result_greet1 = greet_person("سارة")
print(f"الترحيب: {result_greet1}")

result_greet2 = greet_person("أحمد", greeting="صباح الخير")
print(f"الترحيب: {result_greet2}")

print("\n--- التحقق من بيانات الدالة بعد التزيين ---")
print(f"اسم الدالة calculate_sum: {calculate_sum.__name__}")
print(f"توثيق الدالة calculate_sum: {calculate_sum.__doc__}")
print(f"اسم الدالة power: {power.__name__}")
print(f"توثيق الدالة power: {power.__doc__}")

النتيجة المتوقعة

عند تشغيل السكربت، ستلاحظ أن كل دالة مزينة (calculate_sum، power، greet_person) سيتم طباعة رسائل بدء وانتهاء التنفيذ الخاصة بالديكور timer_decorator حول ناتجها الأصلي، بالإضافة إلى وقت التنفيذ لكل دالة.

مثال على جزء من الإخراج:

--- أمثلة على الدوال كمواطنين من الدرجة الأولى ---
سأقوم بتنفيذ الدالة التي تم تمريرها...
أهلاً بك يا علي!
أهلاً بك يا فاطمة!
--------------------------------------------------

--- أمثلة على الدوال المزينة ---
بدء تنفيذ الدالة 'calculate_sum' المزينة...
بدء حساب مجموع الأرقام حتى 1000000 داخل الدالة الأصلية...
انتهى حساب المجموع داخل الدالة الأصلية.
الدالة 'calculate_sum' استغرقت X.XXXX ثانية للتنفيذ.
المجموع المحسوب: 499999500000

بدء تنفيذ الدالة 'power' المزينة...
حساب 2 مرفوعة للأس 10 داخل الدالة الأصلية...
الدالة 'power' استغرقت X.XXXX ثانية للتنفيذ.
الأس المحسوب: 1024

بدء تنفيذ الدالة 'greet_person' المزينة...
تحضير التحية لـ سارة داخل الدالة الأصلية...
الدالة 'greet_person' استغرقت X.XXXX ثانية للتنفيذ.
الترحيب: مرحباً, سارة!

بدء تنفيذ الدالة 'greet_person' المزينة...
تحضير التحية لـ أحمد داخل الدالة الأصلية...
الدالة 'greet_person' استغرقت X.XXXX ثانية للتنفيذ.
الترحيب: صباح الخير, أحمد!

--- التحقق من بيانات الدالة بعد التزيين ---
اسم الدالة calculate_sum: calculate_sum
توثيق الدالة calculate_sum: دالة تحسب مجموع الأرقام حتى n.
اسم الدالة power: power
توثيق الدالة power: دالة تقوم برفع رقم لأس معين.

لاحظ كيف أن رسائل الديكور تظهر قبل وبعد تنفيذ منطق الدالة الأصلي، وكيف أن __name__ و __doc__ للدوال المزينة لا تزال تشير إلى الدوال الأصلية بفضل @functools.wraps.