Generators في Python: ازاي تقرا ملف ضخم بدون ما السكربت يموت
مستوى المقال: مبتدئ — لو لسه فاهم بس الـ for loops والـ functions العادية في Python، الكلام اللي جاي مكتوب على مقاسك. مش هتحتاج تعرف decorators ولا async ولا أي حاجة متقدمة.
لو سكربت Python بتاعك بيموت بـ MemoryError لما يجي يقرا ملف لوج 10GB، السبب مش إن اللاب بتاعك ضعيف. السبب إنك بتقوله "حمّلّي الملف كله في الذاكرة الأول" قبل ما تبدأ تشتغل عليه. Generators بتغيّر ده بكلمة واحدة اسمها yield، وبتنزّل استهلاك الذاكرة من 8.7GB لـ 14MB ثابت — على نفس الملف.
المشكلة باختصار
الكود اللي بتكتبه أول ما تتعلم Python بيشتغل على ملفات صغيرة من غير مشاكل. تيجي على ملف 8GB، نفس الكود بيقع. الفرق بين الحالتين مش في طريقة الكتابة — الفرق في إنك بتفترض إن الذاكرة كبيرة كفاية تستوعب كل البيانات. الافتراض ده بيقع لأول ما حجم الملف يقترب من حجم الـ RAM.
المثال اللي هيخلّيك تفهم Generator في 30 ثانية
تخيّل عامل في فرن عيش بلدي. الزبون قدّامه طلب 100 رغيف. عندك طريقتين:
- الطريقة الأولى (list): العامل يخبز الـ 100 رغيف كلهم الأول، يحطهم في عربية كبيرة، وبعدين يدفع العربية للزبون. لو الفرن مش كبير كفاية يستحمل 100 رغيف في نفس الوقت، الفرن بيقع.
- الطريقة التانية (generator): العامل يخبز رغيف واحد، يديهولك. تاكله أو تحطّه في الكيس. لما تطلب التاني، يخبز التاني. مفيش رغيف موجود في الفرن إلا اللي بتاكله دلوقتي.
الـ Generator هو العامل التاني. بدل ما يحضّر النتيجة كلها في الذاكرة ويسلّمهالك مرة واحدة، بيحضّر قيمة واحدة بس وقت ما تطلبها، وينتظر تطلب التانية.
التعريف العلمي بدون لف
الـ Generator في Python هي function بتستخدم كلمة yield بدل return. الكلمة دي اتعرّفت رسميًا في PEP 255 سنة 2001 تحت اسم "Simple Generators". الفرق التقني واضح:
returnبتنهي الدالة وترجع قيمة واحدة، وحالة الدالة (المتغيرات المحلية) بتتمسح.yieldبتوقف الدالة مؤقتًا، ترجع القيمة، وتحتفظ بحالة الدالة كاملة (المتغيرات، الـ stack، مكان التنفيذ). أول ما الـ caller يطلب القيمة الجاية، الدالة بتكمّل من نفس النقطة.
النتيجة: الذاكرة بتفضل ثابتة طول العملية بصرف النظر عن حجم البيانات.
كود شغّال يبيّن الفرق
اللي تحت كود حقيقي شغّال على Python 3.12. حطّيت الطريقتين جنب بعض علشان تشوف الفرق بعينك:
# الطريقة اللي بتقع الذاكرة على ملفات كبيرة
def read_file_bad(path):
with open(path) as f:
return f.readlines() # بيحمّل الملف كله في list واحد
# الطريقة الصح بـ Generator
def read_file_good(path):
with open(path) as f:
for line in f:
yield line.strip() # سطر واحد بس في الذاكرة في كل لحظة
# الاستخدام واحد، فلسفة الذاكرة مختلفة تمامًا
for line in read_file_good("access_log_8gb.csv"):
if "ERROR" in line:
print(line)