أتقن بروتوكول التكرار والمولّدات لمعالجة تدفقات البيانات معالجة كسولة، وبذاكرة ثابتة.
افتح هذا الدرس في Kodokonليست حلقة for في Python سوى بروتوكول: تطلب iter(obj) مكرِّرًا، ثم يُستدعى next() حتى يُطلَق الاستثناء StopIteration. واحرص على التمييز بين الكائن القابل للتكرار (القائمة، القادرة على تسليم مكرِّر جديد في كل مرور) والمكرِّر (المؤشّر، الذي لا يمكن اجتيازه إلا مرة واحدة). وفهم هذا البروتوكول يعني فهم سبب "استهلاك" بعض الكائنات.
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIterationالدالة التي تحتوي على yield لا تُنفَّذ عند استدعائها: بل تُرجِع مولِّدًا، أي مكرِّرًا يُنفَّذ جسمه عند الطلب. ويتقدّم كل next() إلى yield التالية، حيث يُعلَّق التنفيذ مع كامل حالته المحلية. والنتيجة المباشرة: تبقى الذاكرة ثابتة مهما كان حجم التدفق - فأنت تنتج القيم واحدة تلو الأخرى بدلًا من تجسيد قائمة كاملة.
def squares(limit):
n = 0
while n < limit:
yield n * n
n += 1
gen = squares(4)
print(next(gen)) # 0
print(list(gen)) # [1, 4, 9]الفائدة الرئيسية في الواقع العملي: خطوط المعالجة. تستهلك كل مرحلة تدفق المرحلة السابقة وتنتج تدفقها الخاص، بكسل؛ ولا شيء يُنفَّذ قبل الاستهلاك النهائي (list()، أو sum()، أو حلقة for). ومقايضة يجب تذكّرها: القائمة يمكن اجتيازها عدة مرات وتقطيعها، والمولّد لا؛ في المقابل، يعالج المولّد ملفًا بحجم 10 غيغابايت دون إشباع الذاكرة الحية. أما تعبيرات المولّدات - (f(x) for x in xs if cond) - فتغطي المراحل البسيطة دون تعريف دالة.
lines = [" 12 ", "x", " 7", "", "30"]
def clean(rows):
for row in rows:
row = row.strip()
if row.isdigit():
yield int(row)
def keep_from(values, floor):
return (v for v in values if v >= floor)
pipeline = keep_from(clean(lines), 10)
print(list(pipeline)) # [12, 30]