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
A for loop calls __iter__ once to get an iterator, then calls __next__ repeatedly until StopIteration ends the loop.

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 في هذه الحالة) بين الاستئنافات.

Sequence diagram with two lifelines (caller and generator body). The caller calls count_up_to(3), which creates a generator without running the body. Each subsequent next() runs the body until the next yield, returns the yielded value, and pauses. The fourth next() falls off the end and raises StopIteration. The variable i is preserved across pauses.

تشغّل 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) متى يتوقف.

Three boxes left-to-right: numbers(), squares(source), and the for-v-in-pipeline consumer. Three cycles are drawn beneath. In each cycle, the consumer sends a pull request leftward to squares, which sends a pull leftward to numbers; numbers yields a value rightward to squares, which yields its squared value rightward to the consumer.

كل 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) عاديًا عندما تحتاج حقًا إلى التسلسل بأكمله في الذاكرة.