لو سكربت Python عندك بيقع بـ MemoryError وانت بتقرأ ملف log حجمه 5 جيجا، المشكلة مش في حجم الملف. المشكلة إنك بتحمّله كله في الذاكرة قبل ما تبدأ تشتغل. الـ Generators في Python بتخليك تعالج نفس الملف بـ 50 ميجا ذاكرة فقط، بنفس عدد سطور الكود تقريبًا. هنا هتفهم الفكرة من الصفر، تشوف الفرق بأرقام، وتعرف امتى بالظبط ما تستخدمهاش.
المشكلة بمثال بسيط جدًا
تخيل عندك مكتبة فيها 100 ألف كتاب، وحد طلب منك تجيب الكتب اللي عنوانها فيه كلمة "بايثون". قدامك طريقتين:
- تنزّل الـ 100 ألف كتاب على الأرض الأول، وبعدين تبدأ تدوّر فيهم. ده بيستلزم أرض كبيرة جدًا.
- تاخد كتاب من الرف، تبصّ على عنوانه، لو فيه "بايثون" تحطه جنبك، لو لأ ترجّعه مكانه. وبعدين الكتاب اللي بعده، وهكذا.
الطريقة الأولى بتشغّل ذاكرة ضخمة. الطريقة التانية كتاب واحد في إيدك في كل لحظة. ده بالظبط الفرق بين list و generator في Python.
التعريف العلمي للـ Generator
الـ Generator في Python هو كائن (iterator خاص) بيولّد القيم واحدة وراء التانية، عند الطلب فقط، ومن غير ما يخزّن كل القيم في الذاكرة. ده اسمه lazy evaluation. بيتعرّف بدالة عادية بس بتستخدم كلمة yield بدل return.
الفرق الجوهري بينهم: return بينهي الدالة ويرجع قيمة. yield "بيتنازل" مؤقتًا عن قيمة، ولما المستهلك يطلب التالية، الدالة بتكمل من نفس النقطة بحالتها الداخلية محفوظة (المتغيرات، موقع التنفيذ، كل حاجة). بمعايير PEP 255 الرسمي، الدالة دي اسمها generator function، والكائن اللي بترجعه اسمه generator iterator.
الكود: الطريقة اللي بتكسر الذاكرة، وبديلها
الكود ده شائع جدًا، وبيشتغل تمام على ملف صغير، لكنه بيقع على ملف كبير:
def find_errors(path):
with open(path) as f:
lines = f.readlines() # بيحمّل الملف كله مرة واحدة
return [line for line in lines if "ERROR" in line]
errors = find_errors("server.log") # 5GB → MemoryError
نفس المنطق بالظبط، لكن بـ generator:
def find_errors(path):
with open(path) as f:
for line in f: # الملف نفسه iterator، سطر في الذاكرة
if "ERROR" in line:
yield line # بيتنازل عن السطر، ويكمل بعدين
for err in find_errors("server.log"):
print(err)