المستوى المطلوب: مبتدئ
لو حاولت تقرا ملف log حجمه 10 جيجا في Python بـ readlines() أو بـ pandas.read_csv() عادي، السيرفر بياكل 10 جيجا رام في 4 ثواني وبعدين بيقع بـ MemoryError. Generators بكلمة واحدة اسمها yield بتخلّيك تقرا نفس الملف بـ 8 ميجا رام بس، من غير ما يفرق معاك حجم الملف أصلاً.
Generators في Python: ازاي تخلّي الكود يشتغل "زبون زبون" بدل ما يجيب الناس كلها مرة واحدة
المشكلة باختصار
أي مبتدئ في Python لما بيتعامل مع ملف كبير بيكتب الكود بنفس النمط: افتح الملف، حمّله في الذاكرة، اشتغل عليه. النمط ده شغّال ممتاز على ملفات بضع ميجابايت، لكن أول ما الحجم يكبر، السيرفر بيقف. السبب مش إن Python بطيء، السبب إنك خزّنت الملف كله في الرام مرة واحدة بدل ما تشتغل عليه قطعة قطعة. Generators هي الأداة الجاهزة في اللغة اللي بتحل المشكلة دي بسطر واحد، ومفهومها مش معقّد لو شفته بمثال.
المثال البسيط: المخبز اللي ما يخبزش العيش كله مرة واحدة
تخيّل صاحب مخبز صغير عنده خيارين الصبح.
الخيار الأول: يصحى الساعة 4 الفجر، يخبز 2000 رغيف دفعة واحدة، يحطهم على رفوف ضخمة، وبعدين يستنى الزباين. المشكلة هنا تلاتة: محتاج رفوف كبيرة جدًا تستوعب الكمية، ولو دخل 50 زبون بس النهارده هيتبهدل 1950 رغيف، وأول زبون لازم يستنى ساعتين قبل ما العملية كلها تخلص.
الخيار التاني: يخبز رغيف واحد بس لما زبون يطلب، يدّيهوله ساخن، ويستنى الزبون اللي بعده. مفيش رف، مفيش بهدلة، وأول زبون بياخد رغيفه في 30 ثانية مش بعد ساعتين.
الخيار الأول هو اللي بيعمله list في Python. الخيار التاني هو generator. الفرق بين الكودين كله في كلمة واحدة.
التعريف العلمي بدقة
الـ generator هو دالة في Python بتستخدم كلمة yield بدل return. لما الدالة دي بتتنادى، Python ما بيشغّلش جسمها فورًا. بدل ما يرجّعلك القيمة، بيرجّعلك كائن اسمه generator object. الكائن ده بيشتغل بمبدأ الـ "lazy evaluation": بيحسب القيمة التالية فقط لما حد يطلبها، إما عبر next() أو حلقة for. وبيحفظ مكانه ومتغيراته بين كل نداء وآخر — في PEP 255 ده اسمه "suspended state".
اللي بيحصل عمليًا: استهلاك الذاكرة بيتحوّل من O(n) لـ O(1). يعني مفيش فرق في الذاكرة بين قراءة ملف 1 ميجا أو 1 تيرا — كل لحظة فيه قيمة واحدة بس في الذاكرة.
الكود الفعلي: قراءة ملف 10 جيجا
# الطريقة الغلط - بتاكل 10 جيجا رام
def read_all_lines(path):
with open(path) as f:
return f.readlines() # كل السطور في list في الذاكرة دفعة واحدة
# الطريقة الصح - generator، بياكل بضع كيلوبايت بس
def read_lines_lazy(path):
with open(path) as f:
for line in f:
yield line # سطر سطر، عند الطلب فقط
# الاستخدام في تحليل أخطاء
errors = 0
for line in read_lines_lazy("server.log"):
if "ERROR" in line:
errors += 1
print(f"عدد الأخطاء: {errors}")