Python: المكرّرات والمولّدات
لقد كنت تستخدم
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 — اكتمل التكرار
الناتج:
# Executed successfully
حلقة for تفعل هذا أساساً:
▶ مثال: محاكاة حلقة for يدوياً
numbers = [1, 2, 3]
it = iter(numbers)
while True:
try:
value = next(it)
print(value)
except StopIteration:
break
الناتج:
# Executed successfully
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
الناتج:
# Function defined successfully
▶ مثال: مكرّر فيبوناتشي (الصعوبة ⭐⭐)
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
الناتج:
# Function defined successfully
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
الناتج:
# Function defined successfully
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
الناتج:
# Function defined successfully
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
الناتج:
Generating 1
Generating 2
Generating 3
Generator created, but not yet executed
هل ترى تدفق التنفيذ؟ كل استدعاء لـ 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) # السرعة محدودة فقط بسرعة قراءة القرص
الناتج:
// Executed successfully
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
الناتج:
# Executed successfully
| المقارنة | شاملة قائمة [] |
تعبير مولّد () |
|---|---|---|
| الذاكرة | كل البيانات في الذاكرة | عند الطلب، لا شيء تقريباً |
| السرعة | توليد ووصول سريعان | كسول، يحسب في كل مرة |
| قابلية إعادة الاستخدام | يمكن التكرار عدة مرات | يمكن التكرار مرة واحدة فقط |
| حالة الاستخدام | بيانات صغيرة، استخدام متكرر | بيانات كبيرة، مرور واحد |
6. حالات الاستخدام الشائعة
- معالجة البيانات الكبيرة: اقرأ ملفات سجلات/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للعودية منه.