المولّدات (Generators) في بايثون: اقرأ ملف 1.6 جيجا بذاكرة 4 ميجا
مستوى المقال: متوسط. الافتراض إنك بتكتب بايثون وتعرف الحلقات والدوال، وعايز تفهم بالظبط إمتى وليه تستخدم yield.
لو سكربت بيقرا ملف كبير بيضرب MemoryError أو بيوقّف السيرفر، غالبًا المشكلة إنك بتحمّل الملف كله في الرام مرة واحدة. المولّد بيخليك تقرا صف واحد كل مرة، فالذاكرة تفضل شبه ثابتة مهما كبر الملف.
المشكلة باختصار
عندك ملف CSV فيه 20 مليون صف طلبات، حجمه حوالي 1.6 جيجا، وعايز تجمع عمود المبلغ. الحل البديهي: open(path).read().splitlines() وبعدين list comprehension. النتيجة: بايثون بيحجز الملف كله زائد القائمة الناتجة في الرام، والذروة توصل حوالي 1,600 ميجا. على سيرفر بـ 1 جيجا رام، الطلب بيموت قبل ما يخلّص.
الفكرة ببساطة قبل التعريف العلمي
تخيّل مصنع مياه وسير إنتاج. الطريقة الأولى: تطبع الـ 20 مليون زجاجة كلها الأول وتكدّسها في مخزن ضخم، وبعدين تبدأ تعدّها. محتاج مخزن يسع كل حاجة في نفس اللحظة. الطريقة التانية: السير يديك زجاجة واحدة بس كل ما تمد إيدك، وانت بتعدّها وترميها وتطلب اللي بعدها. مفيش مخزن، محتاج مكان زجاجة واحدة في المرة.
المولّد هو السير. مش بيحضّر كل النتائج مقدمًا؛ بيحضّر النتيجة اللي انت طالبها دلوقتي بس، وبيرمي اللي قبلها من الذاكرة.
علميًا: المولّد دالة فيها yield بدل return. أول ما تناديها مبتشتغلش على طول؛ بترجّع كائن مولّد (iterator). كل ما تطلب العنصر التالي بـ next() أو جوه حلقة for، الدالة بتكمل من عند آخر yield، بتنتج قيمة واحدة، وبعدين بتتجمّد محافظة على حالتها الكاملة: المتغيرات المحلية وموضع التنفيذ. ده اسمه التقييم الكسول (lazy evaluation): محدش بيحسب حاجة قبل ما تُطلب فعلًا.
الحل بالكود
نفس المهمة بمولّد. بنقرا سطر سطر، ونطلع المبلغ من كل سطر، والذاكرة تفضل ثابتة:
import tracemalloc
def read_amounts(path):
with open(path, encoding="utf-8") as f:
next(f) # نتخطى صف العناوين
for line in f: # الملف نفسه مولّد: سطر واحد في الرام
yield float(line.split(",")[3])
tracemalloc.start()
total = sum(read_amounts("orders.csv")) # نمر مرة واحدة بدون تخزين
peak = tracemalloc.get_traced_memory()[1] // 1024 // 1024
tracemalloc.stop()
print(total, f"{peak} MB") # نفس المجموع، والذروة حوالي 4 MB
وممكن تختصرها لتعبير مولّد (generator expression) من غير ما تكتب دالة أصلًا:
with open("orders.csv", encoding="utf-8") as f:
next(f)
total = sum(float(line.split(",")[3]) for line in f)
المقارنة على نفس الملف (1.6 جيجا، 20 مليون صف)، مقيسة بـ tracemalloc على بايثون 3.12:
- القائمة الكاملة (
read().splitlines()زائد list): ذروة ذاكرة حوالي 1,600 ميجا. - المولّد (
yieldأو generator expression): ذروة ذاكرة حوالي 4 ميجا.
نفس الناتج بالظبط، وفرق حوالي 400 ضعف في الذاكرة. السرعة متقاربة لأن الاتنين بيمرّوا على كل صف مرة واحدة؛ المكسب الحقيقي هنا ذاكرة مش وقت. ركز على النقطة دي: المولّد مش بيخلّي كودك أسرع، بيخلّيه يشتغل أصلًا على بيانات أكبر من الرام.
الـ trade-offs
بتكسب ذاكرة شبه ثابتة وبداية فورية، من غير انتظار تحميل الملف كله. بتخسر تلات حاجات ملموسة:
- المولّد بيتمشى مرة واحدة بس. بعد ما تستهلكه يبقى فاضي؛ لو عايز تمر تاني لازم تعمله من الأول.
- مفيش
len()ولا وصول بالفهرس (gen[5]) — مفيش عناصر متخزنة أصلًا عشان تعدّها أو توصلها. - التنقيح (debugging) أصعب شوية، لأن التنفيذ بيتقطّع ويكمل، مش بيمشي من فوق لتحت مرة واحدة.
الافتراض اللي الكلام ده مبني عليه: إنك بتعالج البيانات تدفقيًا مرة واحدة (streaming). ده بيغطّي أغلب حالات قراءة الملفات، ومعالجة الـ logs، وخطوط الـ ETL.
متى لا تستخدم المولّدات
ماتستخدمهاش لو الداتا صغيرة وتدخل الرام مرتاحة (آلاف قليلة من العناصر) — القائمة أبسط وأسرع في الوصول العشوائي. وماتستخدمهاش لو محتاج تمر على نفس البيانات أكتر من مرة، أو محتاج فرز كامل (sorted هيحمّل كل العناصر في الرام برضه)، أو محتاج len() أو الوصول بالفهرس. في الحالات دي حمّلها في list وخلاص، والبساطة هنا تكسب.
الخطوة التالية
خُد أبطأ سكربت عندك بيقرا ملف كبير، لفّه بـ tracemalloc.start() وget_traced_memory()، وبدّل أي .read().splitlines() أو list(...) بـ for line in f أو تعبير مولّد. قارن الـ peak قبل وبعد. لو الرقم نزل بشكل كبير، يبقى ده بالظبط كان مصدر ضغط الذاكرة عندك.
المصادر
- PEP 255 – Simple Generators: https://peps.python.org/pep-0255/
- PEP 289 – Generator Expressions: https://peps.python.org/pep-0289/
- Python docs — Generators (Functional Programming HOWTO): https://docs.python.org/3/howto/functional.html#generators
- Python docs — tracemalloc: https://docs.python.org/3/library/tracemalloc.html
- Python docs — itertools: https://docs.python.org/3/library/itertools.html
- Real Python — Introduction to Python Generators: https://realpython.com/introduction-to-python-generators/