لو عندك ملف CSV بحجم 5 جيجابايت وفتحته بطريقة ساذجة زي f.readlines()، البرنامج بياكل RAM ويوقع. الحل اللي بايثون بيقدمه ببلاش اسمه generators، وهو مش مجرد syntactic sugar. ده بياخد نموذج التنفيذ من eager لـ lazy. هنا هتفهم بالظبط امتى تستخدمه، امتى لا، وإزاي تبني واحد فعلاً شغال في production.
Python Generators بالتفاصيل: من yield لحد الـ streaming pipelines
المشكلة باختصار
لما تكتب list(range(10**8)) بايثون بيحجز ~3.3 جيجابايت ذاكرة علشان يخزّن 100 مليون رقم صحيح. بس لو كتبت range(10**8) من غير list(...)، الذاكرة المستهلكة تقريبًا 48 بايت ثابتة. الفرق مش في الأرقام، الفرق في متى القيم بتتحسب. ده بالظبط اللي generators بتعمله.
eager vs lazy: الفرق اللي بيغير شكل الكود
الـ list في بايثون eager: بتتحسب كل عناصرها وبتتخزن في الذاكرة وقت الإنشاء. الـ generator lazy: القيم بتتولّد بس لما حد يطلبها بـ next() أو بـ for loop. ده بيديك ميزتين:
- استهلاك ذاكرة ثابت تقريبًا، بغض النظر عن حجم البيانات الكلي.
- القدرة على تمثيل سلاسل لا نهائية (infinite streams) من غير ما البرنامج يقع.
إزاي تكتب generator: yield بدل return
أي function فيها كلمة yield بقت generator. ما بتتنفّذش لما تستدعيها، بترجع generator object بتتحرك فيه خطوة بخطوة، وكل مرة بيوصل لـ yield بيوقف ويستنى الاستدعاء التالي.
def read_large_csv(path):
with open(path, "r", encoding="utf-8") as f:
header = next(f).strip().split(",")
for line in f:
values = line.strip().split(",")
yield dict(zip(header, values))
# الاستخدام: بيقرا سطر سطر، مش الملف كامل
for row in read_large_csv("events_5gb.csv"):
if row["status"] == "error":
print(row["user_id"], row["message"])
الكود ده بيشتغل على ملف 5 جيجا بذاكرة تقريبًا 12 ميجا ثابتة خلال كل التنفيذ. الفرق الجوهري: الملف ما بيتحملش في RAM، بيتقرا streaming من الـ disk سطر بسطر.
generator expressions: lazy list comprehension
بدل list comprehension بـ []، استخدم () وهتلاقي نفس المنطق بس lazy: