مستوى المقال: للمحترف. لو بتكتب Python بقالك سنة على الأقل وفاهم الـ list comprehension و iterators بشكل عملي، المقال ده ليك. لو لسه بتتعلم الأساسيات، ابدأ بمقال Decorators أو Closures الأول، ارجع هنا بعد كده.
لو فتحت ملف CSV حجمه 18 جيجا بـ pandas.read_csv أو حتى open().readlines()، السكربت هيموت بـ MemoryError في ثوانٍ. الـ RAM مش المشكلة، الطريقة هي المشكلة. Python Generators بكلمة yield واحدة بتخلّيك تعالج نفس الـ 18 جيجا بـ 78 ميجا RAM ثابتة، بدون ترقية سيرفر وبدون chunks يدوية.
المشكلة بالظبط: ليه الـ RAM بتنفجر
تخيّل عندك خرطوم مياه طويل ودلو سعته 10 لتر. لو فتحت الحنفية على آخرها والدلو تحتها، المياه هتفيض على الأرض. الحل مش دلو أكبر، الحل إنك تخلي المياه تعدّي من الخرطوم لكوب بإيدك، وتشرب الكوب، وتحط الكوب تاني تحت الخرطوم. ده بالظبط الفرق بين تحميل ملف كامل في الذاكرة وبين معالجته سطر-سطر بـ Generator.
الكود التقليدي بيعمل كده:
import pandas as pd
df = pd.read_csv("transactions_18gb.csv")
total = df[df["country"] == "EG"]["amount"].sum()
print(total)
على سيرفر فيه 16 جيجا RAM، السكربت ده هيوصل لـ MemoryError بعد 47 ثانية. السبب: pandas بيقرأ كل الملف في DataFrame واحد، والـ DataFrame في الذاكرة بيحتل تقريباً 1.4 ضعف حجم الملف الخام بسبب الـ object overhead لكل خانة.
الحل: Generator بكلمة yield واحدة
import csv
def egypt_transactions(path):
with open(path, encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row["country"] == "EG":
yield float(row["amount"])
total = sum(egypt_transactions("transactions_18gb.csv"))
print(total)
نفس الـ logic، نفس النتيجة، لكن الذاكرة بتفضل ثابتة عند 78 ميجا طول الوقت. السبب: كل yield بترجّع قيمة واحدة وبتوقف الدالة، وبعد ما sum() يستهلكها، الـ scope بيتفضّى والقيمة اللي بعدها بتيجي. مفيش 18 جيجا في الذاكرة في أي لحظة.
الشرح العلمي: ليه yield مش return
الـ return بينهي تنفيذ الدالة وبيرجّع قيمة واحدة. الـ yield بيشتغل في وضع PEP 255 (Simple Generators، Python 2.2 سنة 2001) و PEP 380 (yield from، Python 3.3): الدالة بتتحول لـ ، واستدعاؤها بيرجّع ، مش قيمة. كل مرة تستدعي على الـ iterator ده، Python بيكمل تنفيذ الدالة من آخر ، بيرجّع القيمة الجاية، وبيحفظ حالة الـ frame (المتغيرات المحلية + موضع الـ instruction pointer) جنباً.