دالة تتوقف في منتصف تنفيذها
المُولِّد (generator) في بايثون يشبه أي دالة عادية في شكله، لكنه يختلف عنها جوهريًا: بدلًا من أن يحسب النتيجة كاملة ثم يعيدها دفعة واحدة، فإنه يُنتج (yield) قيمة واحدة في كل مرة، ثم يتوقف مؤقتًا حتى يطلب منه المستدعي القيمة التالية.
وهذا أبسط مثال ممكن عليه:
لاحظ استخدام yield بدلاً من return. في أول مرة تطلب فيها حلقة for قيمة، تنفّذ بايثون جسم الدالة حتى تصل إلى yield 1، وهنا تتوقف الدالة عند هذه النقطة بالضبط، وتُسلّم القيمة 1 إلى الحلقة، مع الاحتفاظ بموقعها كاملاً — بما في ذلك قيم المتغيرات. في التكرار التالي تستأنف الدالة من حيث توقفت: current += 1، ثم تعود إلى while، ثم yield 2، وهكذا إلى أن يصبح شرط الحلقة غير محقق، فيتوقف المولّد ببساطة.
هذا الإيقاف والاستئناف هو جوهر الفكرة كلها.
لماذا لا نكتفي ببناء قائمة؟
لأن نسخة القائمة تُخصّص كل القيم في الذاكرة دفعة واحدة من البداية:
الأمر مقبول مع 5 عناصر. لكن تخيّل أنك تريد 50 مليون عدد صحيح، وكل ما يهمك هو أول قيمة تحقق شرطًا معينًا. نسخة list ستخصّص 50 مليون عنصر في الذاكرة لترمي معظمها بعد ذلك. أما نسخة المولّد (generator) فتُنشئ بالضبط العدد الذي يستهلكه المستدعي. وعندما تعثر حلقة for على ما تريد وتُنفّذ break، يتوقّف المولّد ببساطة.
هذا هو النمط الذي يستحق أن ترسّخه في ذهنك: المولدات في بايثون تتيح لك كتابة منطق التكرار دون أن تُقرّر مسبقًا كم من النتائج ستحتاج فعلًا.
تعابير المولّدات (Generator Expressions)
إذا سبق أن كتبت list comprehension، فأنت تعرف الصياغة مسبقًا — فقط استبدل الأقواس المربّعة بأقواس عادية:
squares_gen لا يحسب أي شيء حتى الآن، بل هو مجرد وصفة جاهزة. التكرار عليه هو ما ينفّذ الوصفة خطوة بخطوة.
تتألّق تعبيرات المولّدات (generator expressions) حين نمرّرها كوسيط لدوال تستهلك كائنًا قابلًا للتكرار:
بدون قائمة وسيطة. دوال sum وmax وany تقرأ القيم واحدة تلو الأخرى، وهذا بالضبط ما تحتاجه.
قراءة ملف ضخم سطرًا بسطر
هذه هي الحالة الكلاسيكية التي تتألق فيها المولدات في بايثون على أرض الواقع — معالجة ملف أكبر من أن يُحمَّل كاملًا في الذاكرة:
def parse_log_lines(path):
with open(path) as f:
for line in f:
if line.startswith("ERROR"):
yield line.rstrip()
for error in parse_log_lines("app.log"):
print(error)
تتم قراءة الملف بأسلوب التقييم الكسول. كل استدعاء للمولد يسحب سطراً واحداً من القرص، يفلتره، ثم يُرجعه عبر yield. استهلاك الذاكرة يظل ثابتاً مهما كبر حجم الملف.
المولد يُستهلك مرة واحدة فقط
المولد في بايثون يُمرَّر عليه مرة واحدة لا غير. بمجرد أن تصل إلى نهايته، يكون قد استُنفد بالكامل:
الحلقة الثانية لا تطبع أي شيء، لأن المولّد قد استُهلك بالكامل ولم يعد فيه قيم.
إذا احتجت إلى المرور على القيم أكثر من مرة، فأمامك خياران: إمّا استدعاء دالة المولّد من جديد للحصول على مولّد جديد، أو تحويل التسلسل إلى قائمة عبر list(...) ثم التكرار عليها كما تشاء. الاختيار يعتمد على التكلفة: إعادة البناء مناسبة إن كانت العملية رخيصة، والقائمة مناسبة إن كان التسلسل صغيرًا.
الدالة next() والتكرار اليدوي
لست مضطرًا لاستخدام حلقة for، فالدالة next() تتيح لك سحب قيمة واحدة في كل مرة:
StopIteration هي الطريقة التي يقول بها المولّد: "خلصت شغلي." حلقات for تلتقط هذا الاستثناء بصمت دون أي ضجة. أما في الكود اليدوي، فتقدر تمرّر قيمة افتراضية عبر next(gen, default) لتتفادى الاستثناء.
المولدات اللانهائية في بايثون
بما أن القيم تُنتَج عند الطلب فقط (lazy evaluation)، يقدر المولّد أن يمثّل سلسلة بلا نهاية — طالما أن المستهلك توقّف عن طلب المزيد:
while True مع yield داخله لا يُعلّق البرنامج — معناه ببساطة: "طالما في أحد يطلب، استمر في الإنتاج". المستهلك هو من يقرر متى يتوقف.
هذا النمط تلاقيه كثيرًا في تدفّق البيانات (streaming)، وحلقات الأحداث (event loops)، وأي مصدر تسحب منه قيمًا بدون طول محدّد مسبقًا.
yield from: تفويض مولّد آخر
لو أردت أن يُنتج المولّد كل القيم القادمة من iterable آخر، فإن yield from يختصر لك ذلك في سطر واحد:
بدون yield from كنت ستكتب حلقة for متداخلة فيها yield x. كما أنها تمرّر استدعاءات send() و throw() بالشكل الصحيح إن احتجتها يومًا — لكن في الكود اليومي، تعامل معها ببساطة على أنها "أعطِ كل القيم القادمة من هذا الشيء".
متى تستخدم المولدات في بايثون؟
هناك ثلاث إشارات تدلّك على أن المولّد (generator) هو الأداة المناسبة:
- التسلسل كبير، أو ربما لانهائي، أو مكلف لو أنتجته دفعة واحدة.
- المستهلك قد يتوقف قبل الوصول إلى النهاية (مثلًا
breakعند أول تطابق). - تريد ربط سلسلة من التحويلات — تصفية، تحويل، أخذ جزء — دون بناء قوائم وسيطة.
ومتى لا تستخدمها؟
- عندما تحتاج إلى وصول عشوائي (
seq[42]). المولدات تسير إلى الأمام فقط. - عندما تحتاج إلى المرور على نفس التسلسل أكثر من مرة. استخدم قائمة.
- عندما يكون التسلسل صغيرًا وموجودًا لديك أصلًا. في هذه الحالة list comprehension أبسط وأوضح.
كلٌ من المولدات و list comprehensions والقوائم العادية هو الإجابة الصحيحة في موقف مختلف. المهارة تكمن في اختيار الأداة المناسبة دون تفكير طويل — وأسرع طريقة لبناء هذا الحدس هي أن تسأل نفسك مع كل تكرار تكتبه: هل الأنسب هنا "أنتج كل شيء أولًا" أم "أنتج عنصرًا واحدًا في كل مرة"؟
التالي: مديرو السياق بالتفصيل
إلى هنا تكون قد تعرّفت على معظم الأنماط التي تستخدمها بايثون في التكرار. التالي هو مديرو السياق (context managers) — أي عبارة with — وهي تنسجم جيدًا مع المولدات عند بثّ البيانات من الملفات واتصالات الشبكة.
الأسئلة الشائعة
ما هو المولّد (generator) في بايثون؟
المولّد هو دالة تُنتج القيم واحدة تلو الأخرى مع التوقّف بينها. تكتبها بـ def كأي دالة عادية، لكنك تستخدم yield بدلاً من return. عند استدعائها تحصل على كائن مولّد، وكل تكرار في حلقة for أو كل استدعاء لـ next() يُشغّل الدالة حتى الوصول إلى yield التالي.
ما الفرق بين القائمة (list) والمولّد (generator)؟
القائمة تحتفظ بكل العناصر في الذاكرة دفعة واحدة، بينما المولّد يحسب العناصر عند الطلب وينساها بعد استهلاكها. للسلاسل الضخمة أو اللانهائية، يستهلك المولّد قدراً ثابتاً صغيراً من الذاكرة؛ أما إذا كانت النتائج صغيرة وتحتاجها أكثر من مرة، فالقائمة هي الأنسب.
هل يمكنني المرور على المولّد أكثر من مرة؟
لا. المولّد يُستهلك بالكامل بعد أول مرور، ومحاولة تشغيل حلقة for ثانية عليه لن تُنتج شيئاً. إذا احتجت التكرار أكثر من مرة، فاستدعِ دالة المولّد مجدداً للحصول على مولّد جديد، أو حوِّل النتائج إلى قائمة باستخدام list().