المُزيّنات والإغلاقات

المُزيّنات هي واحدة من أكثر ميزات بايثون أناقة. تتيح لك "إضافة أشياء إضافية" للدوال دون تعديل الكود الأصلي — إضافة تسجيل، توقيت، فحوصات أذونات... العديد من الأطر والمكتبات تستخدم المُزيّنات بشكل مكثف. فهمها هو علامة فارقة مهمة في إتقان بايثون.


1. الإغلاقات

الإغلاقات هي أساس المُزيّنات. ببساطة: دالة داخلية تتذكر متغيرات من الدالة الخارجية، حتى بعد انتهاء تنفيذ الدالة الخارجية.

PYTHON
def make_multiplier(n):
    """أنشئ دالة ضرب"""
    def multiplier(x):
        return x * n        # الدالة الداخلية تتذكر المتغير الخارجي n
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))            # 10
print(triple(5))            # 15
print(double(10))           # 20

على الرغم من أن double و triple تأتيان من نفس دالة المصنع make_multiplier، كل منهما تتذكر قيماً مختلفة لـ n (2 و 3). هذا هو الإغلاق — دالة + المتغيرات الخارجية التي تتذكرها.

💡 حالات استخدام الإغلاق: 1) دوال المصنع (إنشاء دوال مهيأة بشكل مختلف كما أعلاه). 2) المُزيّنات (التنفيذ القياسي). 3) حفظ السياق في دوال الاستدعاء العكسي.


2. أساسيات المُزيّنات

المُزيّن هو دالة تأخذ دالة كوسيطة وتُعيد دالة جديدة:

PYTHON
def my_decorator(func):
    def wrapper():
        print("=== Before function execution ===")
        func()
        print("=== After function execution ===")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()

المخرجات:

TEXT
=== Before function execution ===
Hello!
=== After function execution ===

@my_decorator تعادل say_hello = my_decorator(say_hello). السكر النحوي يجعل الكود أنظف.

مثال: مُزيّن تسجيل (الصعوبة ⭐⭐)

PYTHON
def log_call(func):
    """سجّل استدعاءات الدوال"""
    def wrapper(*args, **kwargs):
        print(f"[LOG] Calling {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} returned {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

@log_call
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

print(add(3, 5))
print(greet("Zhang San"))
▶ جرّب الكود

المخرجات:

TEXT
[LOG] Calling add((3, 5), {})
[LOG] add returned 8
8
[LOG] Calling greet(('Zhang San',), {'greeting': 'Hello'})
[LOG] greet returned Hello, Zhang San!

3. functools.wraps: الحفاظ على معلومات الدالة الأصلية

المُزيّنات "تستبدل" الدالة الأصلية، مما يتسبب في فقدان اسم الدالة وسلسلة التوثيق. @wraps يصلح هذا:

PYTHON
from functools import wraps

def my_decorator(func):
    @wraps(func)                    # حافظ على معلومات الدالة الأصلية
    def wrapper(*args, **kwargs):
        """دالة المُزيّن الداخلية"""
        print("Before function execution...")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def say_hello():
    """دالة ترحيب بسيطة"""
    print("Hello!")

print(say_hello.__name__)           # say_hello (بدون @wraps، سيظهر wrapper)
print(say_hello.__doc__)            # دالة ترحيب بسيطة (بدون @wraps، سيظهر دالة المُزيّن الداخلية)
💡 أضف دائماً @wraps عند كتابة المُزيّنات. بدونها، يُفقد اسم الدالة المُزيّنة وتوثيقها، مما يجعل التصحيح صعباً. استورد wraps من functools وطبّق @wraps(func) على الدالة الداخلية (wrapper).


4. المُزيّنات المُعَلمة

أحياناً المُزيّن نفسه يحتاج معاملات:

PYTHON
from functools import wraps

def repeat(times=2):
    """كرّر التنفيذ عدداً محدداً من المرات"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("Zhang San")

المخرجات:

TEXT
Hello, Zhang San!
Hello, Zhang San!
Hello, Zhang San!

مثال: مُزيّن التوقيت (الصعوبة ⭐⭐⭐)

PYTHON
import time
from functools import wraps

def timer(func):
    """احسب وقت تنفيذ الدالة"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"[Timer] {func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    total = 0
    for i in range(1000000):
        total += i
    return total

result = slow_function()
print(f"Result: {result}")
▶ جرّب الكود

المخرجات (يختلف التوقيت حسب الحاسوب):

TEXT
[Timer] slow_function took 0.0452 seconds
Result: 499999500000

5. دمج مُزيّنات متعددة

يمكن للدالة أن تحتوي على مُزيّنات متعددة مكدسة. ترتيب التنفيذ من الأسفل إلى الأعلى:

PYTHON
from functools import wraps

def bold(func):
    @wraps(func)
    def wrapper():
        return f"<b>{func()}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper():
        return f"<i>{func()}</i>"
    return wrapper

@bold
@italic
def hello():
    return "Hello"

print(hello())              # <b><i>Hello</i></b>
# ترتيب التنفيذ: أولاً @italic، ثم @bold

# تعادل: hello = bold(italic(hello))
💡 المُزيّنات المتعددة تُنفّذ من الأسفل إلى الأعلى. فكر في الدالة على أنها "تُغلّف" طبقة طبقة — المُزيّن السفلي الأقرب للدالة الأصلية يُنفّذ أولاً. فهم هذا الترتيب يمنع المُزيّنات المتعددة من إحداث ارتباك.


حالات الاستخدام الشائعة


❓ أسئلة شائعة

س هل *args و kwargs ضروريان في المُزيّنات؟
ج إذا كان المُزيّن الخاص بك يغلّف دوالاً بعدد غير معروف من المعاملات، نعم — استخدم *args, kwargs لتمرير جميع الوسائط. إذا كنت تغلّف فقط دوالاً بتوقيع محدد، يمكنك تسمية معاملات محددة، لكن هذا أقل عمومية. المُزيّنات العامة تستخدم دائماً تقريباً *args, **kwargs. س:ما العلاقة بين المُزيّنات والإغلاقات؟ ج:المُزيّنات مُنفّذة باستخدام الإغلاقات. الإغلاق هو "دالة تتذكر المتغيرات الخارجية." المُزيّنات تستفيد من الإغلاقات حتى تتمكن الدالة الداخلية (wrapper) من الوصول إلى معامل func المُمرّر من الدالة الخارجية. س:ما هي المُزيّنات المفيدة التي تأتي مع بايثون؟ ج:@property (الوصول إلى السمات)، @staticmethod (الدوال الثابتة)، @classmethod (دوال الفئة)، @functools.lru_cache() (التخزين المؤقت)، @functools.wraps (الحفاظ على معلومات الدالة)، @dataclass (فئات البيانات). جميعها في المكتبة القياسية.

📖 ملخص

  • الإغلاق: دالة داخلية تتذكر متغيرات الدالة الخارجية، حتى بعد انتهاء تنفيذ الدالة الخارجية
  • المُزيّن: دالة تأخذ دالة وتُعيد دالة؛ تستخدم السكر النحوي @
  • @wraps(func) يحافظ على __name__ و __doc__ الأصليين للدالة — استخدمه دائماً في المُزيّنات
  • المُزيّنات المُعَلمة: ثلاثة مستويات من التداخل — الخارجي يستقبل المعاملات، الأوسط يستقبل الدالة، الداخلي يعالج المنطق
  • المُزيّنات المتعددة تُطبّق من الأسفل إلى الأعلى
  • المُزيّنات هي "دوال الدوال" — تعزيز الدوال دون تعديل الكود الأصلي

📝 تمارين

  1. مبتدئ (الصعوبة ⭐): اكتب مُزيّناً print_call يطبع "جاري استدعاء دالة xxx" عند تنفيذ الدالة.

  2. متوسط (الصعوبة ⭐⭐): اكتب مُزيّناً retry(max_attempts=3) يُعيد المحاولة تلقائياً العدد المحدد من المرات عندما تُثير الدالة استثناءً. إذا استنفدت المحاولات، ارفع الاستثناء النهائي. تلميح: استخدم حلقة for + try-except في الدالة الداخلية (wrapper).

  3. متقدم (الصعوبة ⭐⭐⭐): اكتب مُزيّناً مُعَلمًا cache_result(ttl=60) يخبّئ قيمة إرجاع الدالة. خلال ثواني TTL، استدعاء بنفس المعاملات يُرجع النتيجة المخبأة؛ بعد TTL، أعد حساب القيمة.

100%