2.31. المُكرِّرات والمولّدات¶
كانت حلقة for تؤدي عملًا أكثر مما يبدو. تغطي هذه الصفحة بروتوكول المُكرِّر (iterator protocol) الذي تعمل عليه، والكلمة المفتاحية yield التي تتيح لك بناء مُكرِّراتك الخاصة.
2.31.1. بروتوكول المُكرِّر¶
كل كائن يمكن التكرار عليه يطبّق طريقتين:
__iter__()-- تُعيد مُكرِّرًا على عناصر الكائن.__next__()-- على المُكرِّر، تُعيد العنصر التالي أو تطلقStopIterationعندما لا يبقى المزيد.
تستدعي الدالة المُدمجة iter() طريقة __iter__؛ وتستدعي next() طريقة __next__. تنقّل عبر قائمة يدويًا:
it = iter([10, 20, 30])
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
print(next(it)) # raises StopIteration
for هي اختصار لـ "استدعِ __iter__ مرة واحدة، ثم كرّر على __next__ حتى StopIteration."¶
ما تفعله for x in items: فعليًا:
_it = iter(items)
while True:
try:
x = next(_it)
except StopIteration:
break
# ... loop body ...
كل قائمة، وصف (tuple)، وسلسلة نصية، وقاموس (dict)، ومجموعة (set)، وكائن ملف، ومولّد يطبّق بالفعل __iter__ و __next__ -- ولهذا تعمل جميعها مع for.
2.31.2. yield ودوال المولّدات¶
الدالة التي تحتوي على عبارة yield هي دالة مولّد. استدعاؤها لا يشغّل جسمها؛ بل يُعيد كائن مولّد (مُكرِّر) يشغّل الجسم بمعدل yield واحدة في كل مرة:
def count_up_to(n):
i = 0
while i < n:
yield i
i += 1
for value in count_up_to(3):
print(value)
الخرج:
0
1
2
كل استدعاء لـ next() يستأنف الدالة حتى yield التالية، ويُسلّم تلك القيمة إلى المستدعي، ثم يتوقف مؤقتًا هناك. وتُحفظ الحالة المحلية (i في هذه الحالة) بين الاستئنافات.
تشغّل next() الجسم حتى yield التالية، وتُسلّم القيمة، ثم تتوقف مؤقتًا. وتبقى الحالة المحلية صامدة عبر التوقف.¶
المولّدات هي أسهل طريقة لإنتاج تسلسل بشكل كسول (lazily) -- إذ لا تُبنى أي قائمة، وتُحسب العناصر فقط عندما يطلبها المستهلك، ويمكن للدالة أن تنتج عناصر إلى ما لا نهاية إن أرادت.
2.31.3. خطوط الأنابيب الكسولة¶
تتركّب المولّدات جيدًا. إذ يمكن لخرج مولّد واحد أن يغذّي مولّدًا آخر:
def numbers():
i = 0
while True:
yield i
i += 1
def squares(source):
for x in source:
yield x * x
pipeline = squares(numbers())
for v in pipeline:
if v > 100:
break
print(v)
تتدفق القيم عبر خط الأنابيب واحدة تلو الأخرى -- بلا قائمة وسيطة، وبلا حد أعلى مدمج في numbers، ويقرر المستهلك (for v in pipeline) متى يتوقف.
كل next() على المستهلك يطلق عملية سحب واحدة عبر السلسلة؛ ولا توجد القيم إلا عندما يطلبها شيء ما.¶
2.31.3.1. yield from¶
الحلقة التي تسحب عناصر من كائن قابل للتكرار آخر وتنتج كل عنصر منها شائعة بما يكفي لأن يوفّر Python اختصارًا لها. التعبير yield from iter ينتج كل قيمة يولّدها الكائن القابل للتكرار، بالترتيب -- كما لو كان لدى المولّد حلقة for x in iter: yield x مضمّنة:
def chain(*sources):
for source in sources:
yield from source
for v in chain([1, 2, 3], (4, 5), "abc"):
print(v)
الخرج:
1
2
3
4
5
a
b
c
yield from مكافئة تمامًا لحلقة for الصريحة، لكنها أقصر، وهي تنشر StopIteration من الكائن القابل للتكرار الداخلي إلى المولّد الخارجي بشكل نظيف -- وهو أمر مفيد عند ربط عدة مولّدات من طرف إلى طرف.
2.31.3.2. عندما تنفد yield¶
تجاوز نهاية دالة المولّد (أو الوصول إلى return صريحة) يطلق StopIteration تلقائيًا. لا حاجة لإطلاقها يدويًا؛ إذ تراها حلقة for المحيطة وتنتهي.
استخدم المولّدات عندما تُكتب الشيفرة المنتِجة بشكل طبيعي كحلقة ذات بضع نقاط إنتاج؛ واستخدم استيعاب قائمة (list comprehension) عاديًا عندما تحتاج حقًا إلى التسلسل بأكمله في الذاكرة.