المكرّرات والمولّدات

لقد كنت تستخدم for x in list: لتكرار البيانات، لكن هل فكرت يوماً في ما يمكن أن يتبع in؟ لماذا لا يستهلك range(1000000) ذاكرة؟ المكرّرات و المولّدات هي الإجابة. تتيح لك التعامل مع تدفقات بيانات "لا نهائية" — تُنتَج عند الطلب، لا تُخزّن في الذاكرة.


1. ما هو بروتوكول المكرّر

في بايثون، أي كائن يمكن استخدامه في حلقة for هو قابل للتكرار. إنها تنفذ بروتوكول المكرّر:

مثال: أساسيات بروتوكول المكرّر

PYTHON
# جميع الحاويات قابلة للتكرار
print(hasattr([1, 2, 3], "__iter__"))     # True
print(hasattr("abc", "__iter__"))          # True
print(hasattr(100, "__iter__"))            # False — الأرقام غير قابلة للتكرار

# عملية التكرار اليدوية
numbers = [1, 2, 3]
iterator = iter(numbers)     # احصل على مكرّر

print(next(iterator))        # 1
print(next(iterator))        # 2
print(next(iterator))        # 3
# print(next(iterator))      # StopIteration — اكتمل التكرار
▶ جرّب الكود

حلقة for تفعل هذا أساساً:

مثال: محاكاة حلقة for يدوياً

PYTHON
numbers = [1, 2, 3]
it = iter(numbers)
while True:
    try:
        value = next(it)
        print(value)
    except StopIteration:
        break
▶ جرّب الكود

2. مكرّرات مخصصة

نفّذ __iter__ و __next__ لجعل فصلك يعمل مع حلقات for:

مثال: مكرّر العد التنازلي

PYTHON
class Countdown:
    """مكرّر العد التنازلي"""
    def __init__(self, start):
        self.current = start
    
    def __iter__(self):
        return self                     # المكرّر يُرجع نفسه
    
    def __next__(self):
        if self.current <= 0:
            raise StopIteration         # أوقف التكرار
        value = self.current
        self.current -= 1
        return value

# الاستخدام
for i in Countdown(5):
    print(i, end=" ")                   # 5 4 3 2 1
▶ جرّب الكود

مثال: مكرّر فيبوناتشي (الصعوبة ⭐⭐)

PYTHON
class Fibonacci:
    """مكرّر فيبوناتشي — يُنشئ أول N أرقام"""
    def __init__(self, count):
        self.count = count
        self.index = 0
        self.a, self.b = 0, 1
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index >= self.count:
            raise StopIteration
        self.index += 1
        result = self.a
        self.a, self.b = self.b, self.a + self.b
        return result

for num in Fibonacci(10):
    print(num, end=" ")                 # 0 1 1 2 3 5 8 13 21 34
▶ جرّب الكود

3. المولّدات: yield

كتابة فئة بـ __iter__ و __next__ مملة. دوال المولّدات تستخدم yield لتحقيق نفس الشيء:

مثال: مولّد العد التنازلي

PYTHON
def countdown(start):
    """مولّد العد التنازلي"""
    while start > 0:
        yield start
        start -= 1

# الاستخدام
for i in countdown(5):
    print(i, end=" ")                   # 5 4 3 2 1
▶ جرّب الكود

yield يختلف عن returnreturn ينهي الدالة، بينما yield يُوقفها مؤقتاً، ويتذكر الحالة الحالية، ويستأنف من حيث توقف في الاستدعاء التالي.

مثال: مولّد فيبوناتشي

PYTHON
# نفس فيبوناتشي — باستخدام yield، أكثر إيجازاً بكثير من فئة المكرّر
def fibonacci(count):
    a, b = 0, 1
    for _ in range(count):
        yield a
        a, b = b, a + b

for num in fibonacci(10):
    print(num, end=" ")                 # 0 1 1 2 3 5 8 13 21 34
▶ جرّب الكود
💡 المولّدات مقابل فئات المكرّرات: دوال المولّدات (yield) عادة ما تكون أكثر إيجازاً بكثير من كتابة فئات المكرّر يدوياً. في معظم السيناريوهات، المولّدات هي الخيار الأول. فكر فقط في كتابة فئة مكرّر إذا كنت بحاجة للحفاظ على حالة معقدة أو تحتاج دوالاً إضافية.


4. التحميل الكسول مع المولّدات

أكبر ميزة للمولّدات — لا تُنشئ جميع البيانات مرة واحدة، بل عند الطلب:

مثال: التحميل الكسول للمولّد

PYTHON
def generate_numbers():
    """محاكاة توليد الأرقام واحداً تلو الآخر"""
    print("Generating 1")
    yield 1
    print("Generating 2")
    yield 2
    print("Generating 3")
    yield 3

gen = generate_numbers()
print("Generator created, but not yet executed")

print(next(gen))    # Generating 1 \n 1
print(next(gen))    # Generating 2 \n 2
print(next(gen))    # Generating 3 \n 3
▶ جرّب الكود

هل ترى تدفق التنفيذ؟ كل استدعاء لـ next() ينفذ حتى yield التالي، ثم يتوقف. هذا مفيد بشكل لا يصدق للبيانات الكبيرة:

مثال: قراءة ملف كبير سطراً بسطر

PYTHON
# طريقة "كسولة" لمعالجة الملفات الكبيرة
def read_large_file(filename):
    """اقرأ ملفاً كبيراً سطراً بسطر، دون تحميله كله في الذاكرة"""
    with open(filename, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

# معالجة بالمولّد — سطر واحد فقط في الذاكرة في كل مرة
for line in read_large_file("huge_log.txt"):
    if "ERROR" in line:
        print(line)  # السرعة محدودة فقط بسرعة قراءة القرص
▶ جرّب الكود

5. تعبيرات المولّدات

مشابهة لشاملات القوائم، لكن بـ أقواس — تعبيرات المولّدات تُقيّم بتكاسل:

مثال: تعبير مولّد

PYTHON
# شاملة قائمة — تُنشئ جميع البيانات مرة واحدة
squares_list = [x ** 2 for x in range(1000000)]
print(f"List size: {len(squares_list)}")           # 1000000 (تستهلك ذاكرة)

# تعبير مولّد — عند الطلب
squares_gen = (x ** 2 for x in range(1000000))
print(f"Generator: {squares_gen}")                 # كائن مولّد (لا يستهلك ذاكرة تقريباً)

# نفس الاستخدام — حلقة for
for i, val in enumerate(squares_gen):
    if i >= 5:
        break
    print(val, end=" ")                            # 0 1 4 9 16
▶ جرّب الكود
المقارنة شاملة قائمة [] تعبير مولّد ()
الذاكرة كل البيانات في الذاكرة عند الطلب، لا شيء تقريباً
السرعة توليد ووصول سريعان كسول، يحسب في كل مرة
قابلية إعادة الاستخدام يمكن التكرار عدة مرات يمكن التكرار مرة واحدة فقط
حالة الاستخدام بيانات صغيرة، استخدام متكرر بيانات كبيرة، مرور واحد
💡 متى تستخدم المولّدات؟ ① مجموعات بيانات كبيرة (آلاف+ العناصر). ② تحتاج فقط للتكرار مرة واحدة. ③ تحتاج معالجة متدفقة (ملف سطراً بسطر، تدفقات بيانات في الوقت الفعلي). ④ تريد التعبير عن "تسلسلات لا نهائية."


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


❓ أسئلة شائعة

س ما الفرق بين المولّد والدالة العادية؟
ج الدالة العادية تستخدم return لإرجاع نتيجة مرة واحدة وتنتهي. المولّد يستخدم yield لإنتاج القيم واحدة تلو الأخرى، ويتوقف ويحفظ الحالة بعد كل yield. كل استدعاء لدالة مولّد يُرجع كائن مولّد جديد. س:يمكن تكرار المولّدات مرة واحدة فقط. ماذا لو كنت بحاجة للتكرار عدة مرات؟ ج:استخدم list(gen) لتحويل المولّد إلى قائمة. لكن إذا كانت البيانات كبيرة جداً، التحويل إلى قائمة سيستهلك الذاكرة. حل وسط: استخدم itertools.tee() لنسخ المولّد، أو استدع دالة المولّد مرة أخرى لإنشاء واحد جديد. س:هل يمكن أن يتعايش yield و return؟ ج:نعم — return في المولّد يوقف التكرار فعلياً ويرفع StopIteration بقيمة اختيارية. لكن نادراً ما يُستخدم. إذا وجدت دالة تحتاج كلاً من yield و return، أعد النظر في التصميم.

📖 ملخص

  • الكائنات القابلة للتكرار تدعم حلقات for؛ في الخلفية، تنفذ بروتوكول __iter__ و __next__
  • التكرار اليدوي: iter() يحصل على مكرّر، next() يحصل على القيمة التالية، StopIteration ينهيه
  • دوال المولّدات تستخدم yield لإنتاج القيم واحدة تلو الأخرى، وحفظ الحالة تلقائياً
  • المولّدات كسولة — تُنتج بقدر الحاجة، بدون هدر في الذاكرة
  • تعبيرات المولّدات (x for x in seq) مثل شاملات القوائم، لكن تُقيّم بتكاسل
  • yield from يُفوّض لمولّد آخر (متقدم، تعلمه عندما تحتاجه)

📝 تمارين

  1. أساسي (الصعوبة ⭐): اكتب مولّداً even_numbers(max_n) يُنشئ جميع الأرقام الزوجية من 0 إلى max_n.

  2. متوسط (الصعوبة ⭐⭐): اكتب مولّداً repeat_list(items, times) يُكرّر كل عنصر من قائمة times مرة. مثال: list(repeat_list([1, 2, 3], 2)) يُرجع [1, 1, 2, 2, 3, 3].

  3. تحدي (الصعوبة ⭐⭐⭐): اكتب مولّداً deep_flatten(nested) يُسطح القوائم المتداخلة بشكل تعسفي. مثال: list(deep_flatten([1, [2, [3, 4]], 5])) يُرجع [1, 2, 3, 4, 5]. تلميح: كرّر على العناصر؛ إذا كان العنصر قائمة، استخدم yield from للعودية منه.

100%