المستوى: متوسط — يفترض إنك تعرف Python أساسي وقريت قبل كده ملف بـ open()، بس مش لازم تعرف yield قبل المقال.
Generators في Python: ازاي تقرأ ملف 10GB بـ 12MB ذاكرة
لو سكربت Python عندك بيحاول يقرا ملف log حجمه 10GB ويقع OOM في ثانيتين، المشكلة مش حجم الملف ولا السيرفر. المشكلة إنك بتحمّل كل الملف في الذاكرة دفعة واحدة. Generator بيحلّ المشكلة دي بكلمة واحدة في الكود، وبينزّل الذاكرة من 10GB لـ 12MB على نفس السكربت بالظبط.
المشكلة باختصار
لو فتحت ملف 10GB بـ file.read() أو file.readlines()، Python بيحجز 10GB في الـ RAM قبل ما السطر اللي بعده يتنفّذ. على سيرفر بـ 8GB رام، الـ kernel بيقتل العملية قبل ما تقرا أول سطر. الحل التقليدي: تقسّم الملف يدويًا في loop وتقرأ chunks. الحل الأنظف والأقل كود: yield.
المثال البسيط: المخبز ضد المصنع
تخيّل عندك مخبز صغير. الزبون طلب 100 رغيف خبز. صاحب المخبز ميخبزش 100 دفعة واحدة. بيخبز رغيف، يدّيهولك، ولما تطلب التاني يخبز التاني. لو الزبون بعد 5 أرغفة قال خلاص شكرًا، ميتعملش حاجة للـ 95 الباقيين. ده هو الـ Generator: بينتج قيمة واحدة كل مرة بس لما تطلبها، ولا بيحضّر اللي بعدها قبل الطلب.
المصنع الكبير عكس كده. بيخبز 100 رغيف الأول، يحطهم في صناديق، وبعدين يبدأ يبيع. لو الزبون اشترى 5 وراح، الـ 95 اتحطّوا في الذاكرة من غير فايدة. ده هو الـ list العادي في Python.
الفرق ده هو نفس الفرق بين readlines() و generator في الكود. الأول مصنع، التاني مخبز.
التعريف الدقيق
Generator في Python هو كائن بيطبّق Iterator Protocol بدون ما يخزّن القيم كلها مسبقًا. لمّا تكتب دالة فيها كلمة yield بدل return، Python بيحوّل الدالة لـ generator function. كل مرة تستدعيها، بترجّع generator object — كائن بيحفظ حالة التنفيذ (المتغيرات المحلية والسطر الحالي) ويستأنف من آخر yield لمّا تطلب القيمة التالية بـ next() أو في for loop.
الفرق التقني الأهم: list بيحفظ كل العناصر في الذاكرة فورًا — ذاكرة بحجم N. generator بيحفظ بس الـ frame state — حوالي 200 بايت ثابتة بغضّ النظر عن عدد العناصر اللي ممكن يرجّعها. ممكن يرجّعلك مليار قيمة وهو لسه واخد 200 بايت.
ده اللي بيتسمّى lazy evaluation: الحساب مش بيتعمل لحد ما حد يطلبه فعلًا.
الكود التنفيذي — قبل وبعد
الطريقة الغلط — قراءة الملف كله مرة واحدة: