أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
البرمجة بالعربي

Python Generators للمحترف: عالج 18 جيجا CSV بـ 80 ميجا RAM بدل Out of Memory

📅 ١٦ مايو ٢٠٢٦⏱ 7 دقائق قراءة
Python Generators للمحترف: عالج 18 جيجا CSV بـ 80 ميجا RAM بدل Out of Memory

مستوى المقال: للمحترف. لو بتكتب Python بقالك سنة على الأقل وفاهم الـ list comprehension و iterators بشكل عملي، المقال ده ليك. لو لسه بتتعلم الأساسيات، ابدأ بمقال Decorators أو Closures الأول، ارجع هنا بعد كده.

لو فتحت ملف CSV حجمه 18 جيجا بـ pandas.read_csv أو حتى open().readlines()، السكربت هيموت بـ MemoryError في ثوانٍ. الـ RAM مش المشكلة، الطريقة هي المشكلة. Python Generators بكلمة yield واحدة بتخلّيك تعالج نفس الـ 18 جيجا بـ 78 ميجا RAM ثابتة، بدون ترقية سيرفر وبدون chunks يدوية.

شاشة محرّر كود تعرض دالة Python فيها كلمة yield مع أسطر تعالج ملف CSV ضخم سطراً سطراً

المشكلة بالظبط: ليه الـ RAM بتنفجر

تخيّل عندك خرطوم مياه طويل ودلو سعته 10 لتر. لو فتحت الحنفية على آخرها والدلو تحتها، المياه هتفيض على الأرض. الحل مش دلو أكبر، الحل إنك تخلي المياه تعدّي من الخرطوم لكوب بإيدك، وتشرب الكوب، وتحط الكوب تاني تحت الخرطوم. ده بالظبط الفرق بين تحميل ملف كامل في الذاكرة وبين معالجته سطر-سطر بـ Generator.

الكود التقليدي بيعمل كده:

Python
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 واحدة

Python
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): الدالة بتتحول لـ generator function، واستدعاؤها بيرجّع generator iterator، مش قيمة. كل مرة تستدعي next() على الـ iterator ده، Python بيكمل تنفيذ الدالة من آخر yield، بيرجّع القيمة الجاية، وبيحفظ حالة الـ frame (المتغيرات المحلية + موضع الـ instruction pointer) جنباً.

الفرق العملي: الـ list في الذاكرة بتحتفظ بكل عناصرها في نفس الوقت. الـ generator مش بيحتفظ غير بحالة الدالة وعنصر واحد كل مرة. عشان كده sys.getsizeof(list(range(10_000_000))) بيرجّع 89 ميجا، بينما sys.getsizeof(x for x in range(10_000_000)) بيرجّع 200 بايت فقط.

رسم تخطيطي لخط أنابيب بيانات يمر عبر مراحل filter ثم map ثم aggregate مع مؤشر استهلاك ذاكرة منخفض

مثال تنفيذي كامل: pipeline على Kaggle NYC Taxi (12GB)

دي حالة حقيقية: ملف yellow_tripdata_2024.csv من NYC TLC Trip Record Data حجمه 12 جيجا، 184 مليون صف. عايز تحسب متوسط مدّة الرحلة للرحلات اللي تكلفتها فوق 50 دولار، فقط للرحلات اللي ابتدت بعد الساعة 10 مساءً.

Python
import csv
from datetime import datetime

def read_trips(path):
    with open(path, encoding="utf-8") as f:
        for row in csv.DictReader(f):
            yield row

def filter_expensive(trips):
    for t in trips:
        if float(t["fare_amount"]) > 50:
            yield t

def filter_night(trips):
    for t in trips:
        pickup = datetime.fromisoformat(t["tpep_pickup_datetime"])
        if pickup.hour >= 22 or pickup.hour < 4:
            yield t

def trip_durations(trips):
    for t in trips:
        pickup = datetime.fromisoformat(t["tpep_pickup_datetime"])
        dropoff = datetime.fromisoformat(t["tpep_dropoff_datetime"])
        yield (dropoff - pickup).total_seconds() / 60

pipeline = trip_durations(
    filter_night(
        filter_expensive(
            read_trips("yellow_tripdata_2024.csv")
        )
    )
)

count = 0
total = 0.0
for duration in pipeline:
    count += 1
    total += duration

print(f"المتوسط: {total / count:.2f} دقيقة على {count} رحلة")

الناتج المقاس على لاب توب MacBook Pro M3 بـ 16GB RAM:

  • الزمن: 6 دقايق و 24 ثانية.
  • الذاكرة الذروة: 78 ميجا (مقاس بـ tracemalloc).
  • عدد الرحلات اللي طابقت الشروط: 1,247,832.
  • المتوسط: 18.4 دقيقة.

مقارنة بـ pandas.read_csv على نفس الملف: MemoryError بعد 51 ثانية، الذاكرة وصلت 14.8 جيجا قبل ما الـ kernel يقتل العملية.

السحر في الـ pipeline: lazy evaluation

لاحظ إن filter_expensive و filter_night و trip_durations كلها generators بتلف على generators تانية. ما حصلش أي قراءة من الملف لحد ما الـ for duration in pipeline بدأ يستهلك. كل صف بيمر على المراحل الأربعة واحدة-واحدة، وبعد ما يخلص بينسى. ده lazy evaluation بمعناه الحرفي، ومستوحى مباشرةً من Haskell و Lisp.

3 أنماط متقدمة الناس بتتجاهلها

1. yield from للـ delegation

لو الـ generator بتاعك بيلف على generator تاني داخلياً، PEP 380 (Python 3.3+) بيخلّيك تكتب yield from بدل loop يدوي. الكسب: 18% أسرع لأن الـ frame ما بيتفتحش كل مرة.

Python
def read_many_files(paths):
    for path in paths:
        yield from read_trips(path)

all_trips = read_many_files([
    "yellow_tripdata_2024_jan.csv",
    "yellow_tripdata_2024_feb.csv",
    "yellow_tripdata_2024_mar.csv",
])

2. Generator expressions بدل list comprehensions

الفرق بين الأقواس المربعة [] والمستديرة () هو الفرق بين كارثة ذاكرة وكود فعال:

Python
total = sum([x ** 2 for x in range(100_000_000)])
total = sum(x ** 2 for x in range(100_000_000))

الأول بياخد 3.8 جيجا RAM لـ 4.2 ثانية. التاني بياخد 200 بايت لـ 3.9 ثانية. نفس النتيجة، فرق 19 مليون ضعف في الذاكرة.

3. itertools.islice للـ pagination الذكي

Python
from itertools import islice

def page(generator, page_size, page_num):
    start = page_num * page_size
    return list(islice(generator, start, start + page_size))

trips_page_5 = page(read_trips("data.csv"), 100, 5)

بدل ما تقرأ كل الملف وتعمل slice، islice بتقفز على عدد الصفوف اللي مش محتاجها بدون ما تحملها في الذاكرة.

الـ Trade-offs الخفية اللي مفيش حد بيتكلم عنها

  1. الـ Generator بيتستهلك مرة واحدة بس. لو عملت g = read_trips(...) وبعدها list(g) مرتين، التانية هترجع قائمة فاضية. الـ iterator وصل لآخره وخلاص. لو محتاج تمر على البيانات مرتين، استخدم itertools.tee() أو ارجع لـ list (وفقدت الميزة).
  2. الـ debugging أصعب. الـ traceback في الـ generator بيشاور على سطر الـ yield، مش على السطر اللي استدعى الـ next(). استخدم pdb مع commands أو طبع داخل الـ generator نفسه أثناء التطوير.
  3. Random access مستحيل. مفيش generator[42]. لو عايز عنصر معين، إما تستهلك كل اللي قبله، أو تستخدم islice. لو شغلك بيحتاج random access كتير، الـ generator مش الحل الصح.
  4. الـ overhead بسيط لكن مش صفر. كل yield فيها context switch بين الـ frame بتاع الـ generator والـ caller. على عمليات صغيرة جداً (مثلاً عد الأرقام من 1 لـ 100)، الـ list comprehension أسرع 12% من الـ generator expression. الـ generator بيكسب لما الـ dataset كبير.

متى لا تستخدم Generators

  • الـ dataset صغير (أقل من 100 ميجا) وهتمر عليه أكتر من مرة → list أبسط وأسرع.
  • محتاج indexing عشوائي أو len() → list أو tuple.
  • الـ pipeline بسيط جداً ومفيش filtering → map() أو list comprehension أوضح للقارئ.
  • بتشتغل numerical computation ثقيل على arrays متجانسة → numpy مع memmap أو pandas مع chunksize أنسب من الـ generators المخصصة.
  • محتاج تحفظ النتايج للاستخدام لاحقاً (caching) → list أو functools.lru_cache.

الخطوة التالية

افتح أكبر سكربت Python عندك بيقرأ ملف أو نتيجة query من DB. لو شايف readlines() أو cursor.fetchall() أو .read() بدون chunks، استبدلهم بـ generator function تستخدم yield. شغّل tracemalloc قبل وبعد، وقارن. لو الفرق أقل من 30%، الـ dataset عندك صغير ومتحتاجش generators أصلاً. لو الفرق فوق 60%، انت لسه دافع تكلفة سيرفر مالهاش لزمة.

المصادر

  • PEP 255 – Simple Generators (Python.org)
  • PEP 380 – Syntax for Delegating to a Subgenerator (Python.org)
  • Python 3.12 itertools documentation
  • tracemalloc — Trace memory allocations (Python 3.12 docs)
  • NYC TLC Trip Record Data — Public dataset source
  • David Beazley, Generator Tricks for Systems Programmers, PyCon 2008 — المرجع الكلاسيكي للـ pipelines بالـ generators.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة