المكرّرات والمولّدات
لقد كنت تستخدم
for x in list:لتكرار البيانات، لكن هل فكرت يوماً في ما يمكن أن يتبعin؟ لماذا لا يستهلكrange(1000000)ذاكرة؟ المكرّرات و المولّدات هي الإجابة. تتيح لك التعامل مع تدفقات بيانات "لا نهائية" — تُنتَج عند الطلب، لا تُخزّن في الذاكرة.
1. ما هو بروتوكول المكرّر
في بايثون، أي كائن يمكن استخدامه في حلقة for هو قابل للتكرار. إنها تنفذ بروتوكول المكرّر:
مثال: أساسيات بروتوكول المكرّر
# جميع الحاويات قابلة للتكرار
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 يدوياً
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
value = next(it)
print(value)
except StopIteration:
break
2. مكرّرات مخصصة
نفّذ __iter__ و __next__ لجعل فصلك يعمل مع حلقات for:
مثال: مكرّر العد التنازلي
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
مثال: مكرّر فيبوناتشي (الصعوبة ⭐⭐)
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 لتحقيق نفس الشيء:
مثال: مولّد العد التنازلي
def countdown(start):
"""مولّد العد التنازلي"""
while start > 0:
yield start
start -= 1
# الاستخدام
for i in countdown(5):
print(i, end=" ") # 5 4 3 2 1
yield يختلف عن return — return ينهي الدالة، بينما yield يُوقفها مؤقتاً، ويتذكر الحالة الحالية، ويستأنف من حيث توقف في الاستدعاء التالي.
مثال: مولّد فيبوناتشي
# نفس فيبوناتشي — باستخدام 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
4. التحميل الكسول مع المولّدات
أكبر ميزة للمولّدات — لا تُنشئ جميع البيانات مرة واحدة، بل عند الطلب:
مثال: التحميل الكسول للمولّد
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 التالي، ثم يتوقف. هذا مفيد بشكل لا يصدق للبيانات الكبيرة:
مثال: قراءة ملف كبير سطراً بسطر
# طريقة "كسولة" لمعالجة الملفات الكبيرة
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. تعبيرات المولّدات
مشابهة لشاملات القوائم، لكن بـ أقواس — تعبيرات المولّدات تُقيّم بتكاسل:
مثال: تعبير مولّد
# شاملة قائمة — تُنشئ جميع البيانات مرة واحدة
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
| المقارنة | شاملة قائمة [] |
تعبير مولّد () |
|---|---|---|
| الذاكرة | كل البيانات في الذاكرة | عند الطلب، لا شيء تقريباً |
| السرعة | توليد ووصول سريعان | كسول، يحسب في كل مرة |
| قابلية إعادة الاستخدام | يمكن التكرار عدة مرات | يمكن التكرار مرة واحدة فقط |
| حالة الاستخدام | بيانات صغيرة، استخدام متكرر | بيانات كبيرة، مرور واحد |
حالات الاستخدام الشائعة
- معالجة البيانات الكبيرة: اقرأ ملفات سجلات/CSV كبيرة سطراً بسطر، متجنباً نفاد الذاكرة.
- التسلسلات اللانهائية: يمكن للمولّدات تمثيل تدفقات بيانات "لا نهائية" — فيبوناتشي، بيانات الاستشعار.
- معالجة خط الأنابيب: اربط عدة مولّدات في خط أنابيب معالجة بيانات.
- التكرار العكسي:
reversed()يُرجع مكرّراً لبعض الأنواع. - التقييم الكسول: الدوال تُرجع مولّداً؛ المتصل يقرر عدد القيم التي سيأخذها.
❓ أسئلة شائعة
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يُفوّض لمولّد آخر (متقدم، تعلمه عندما تحتاجه)
📝 تمارين
-
أساسي (الصعوبة ⭐): اكتب مولّداً
even_numbers(max_n)يُنشئ جميع الأرقام الزوجية من 0 إلىmax_n. -
متوسط (الصعوبة ⭐⭐): اكتب مولّداً
repeat_list(items, times)يُكرّر كل عنصر من قائمةtimesمرة. مثال:list(repeat_list([1, 2, 3], 2))يُرجع[1, 1, 2, 2, 3, 3]. -
تحدي (الصعوبة ⭐⭐⭐): اكتب مولّداً
deep_flatten(nested)يُسطح القوائم المتداخلة بشكل تعسفي. مثال:list(deep_flatten([1, [2, [3, 4]], 5]))يُرجع[1, 2, 3, 4, 5]. تلميح: كرّر على العناصر؛ إذا كان العنصر قائمة، استخدمyield fromللعودية منه.



