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

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

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

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

المنصة

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

الدعم

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

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

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

Generators في Python للمتوسط: عالج ملف 50GB بـ 12MB رام بدون ما السيرفر يقع

📅 ٢٥ مايو ٢٠٢٦⏱ 7 دقائق قراءة
Generators في Python للمتوسط: عالج ملف 50GB بـ 12MB رام بدون ما السيرفر يقع

Generators في Python للمتوسط: عالج ملف 50GB بـ 12MB رام بدون ما السيرفر يقع

مستوى المقال: متوسط — هذا الشرح يفترض إنك مرتاح مع for loops و def في Python، ومش لازم تعرف الـ Iterator Protocol مسبقاً. لو لسه بتبدأ Python خالص، احفظ المقال لما تعدي مرحلة الـ functions الأساسية.

شاشة محرر كود بايثون تعرض دالة بكلمة yield مع تدفق بيانات مستمر، يمثّل فكرة Generators ومعالجة البيانات الكبيرة بذاكرة منخفضة

لو سكربت Python بتاعك بيقرا ملف CSV حجمه 50 جيجا والـ OOM Killer قتله بعد 3 دقايق، المشكلة مش في الـ RAM بتاع السيرفر. المشكلة إنك بتحمّل الملف كله في الذاكرة دفعة واحدة بدل ما تقراه سطر سطر. yield واحدة بدل append+return بتنزّل الذاكرة من 47 جيجا لـ 12 ميجا على نفس السيرفر، بدون أي تعديل في الـ infrastructure أو الـ stack.

المشكلة باختصار: ليه الكود التقليدي بياكل الذاكرة كلها

الكود الطبيعي في data processing بيستخدم list لتجميع النتائج. كل عنصر بيتعمله append بيفضل في الذاكرة لحد ما الـ list كلها تترجّع. لو الملف 50 جيجا، الـ list هتاخد 50+ جيجا (Python objects فيها overhead حوالي 28 بايت لكل object صغير حسب sys.getsizeof).

اللي Generator بيعمله مختلف تماماً: بدل ما يحضّر كل النتائج ويرجّعها مرة واحدة، بيرجّع عنصر واحد بس وبيوقّف نفسه في مكانه. لما الكود اللي بيستهلكه يطلب التاني، الـ Generator بيكمل من نفس النقطة. الذاكرة المحجوزة في أي لحظة = حجم العنصر الواحد + state صغير جداً (200-300 بايت لكل generator)، مش حجم الـ collection كاملة.

قبل ما نشرح المفهوم: تخيّل المكتبة العامة

تخيّل إنك في مكتبة فيها 50 ألف كتاب وعايز تدوّر على كلمة معينة. عندك طريقتين:

  1. الطريقة الأولى (List): تطلب من الموظف يجيب كل الكتب على طاولتك دفعة واحدة. هتقعد ساعتين تستنّى، والطاولة هتقع من تحت الـ 50 ألف كتاب.
  2. الطريقة التانية (Generator): الموظف بيجيبلك كتاب واحد، تفتحه، تدوّر على الكلمة، تقول "خلّصت"، فيرجعه ويجيب اللي بعده. الطاولة عليها كتاب واحد بس في أي لحظة، والشغل بيخلص.

دلوقتي بعد ما اتضح المفهوم، نرجع للجانب العلمي بدقة: Generator في Python هو function بتستخدم yield بدل return. أول ما الـ caller يطلب القيمة التالية بـ next() أو بـ for loop، الـ function بتنفّذ لحد أول yield ثم بتتجمّد. الـ state بتاعها (المتغيرات المحلية، الـ instruction pointer، الـ call stack الخاص بيها) بيتحفظ في frame object صغير جداً.

المثال التنفيذي: قراءة CSV 50GB بـ Generator

الكود ده مأخوذ من workload حقيقي لمعالجة logs server في خدمة logistics. الملف اليومي حجمه 47 جيجا (220 مليون سطر JSON Lines).

صفوف خوادم في مركز بيانات مع أضواء زرقاء، تمثّل سيناريو معالجة ملف لوجز ضخم 50 جيجابايت داخل سيرفر إنتاج

الكود الغلط - بيموّت السيرفر

Python
def parse_logs_bad(path):
    rows = []
    with open(path, 'r') as f:
        for line in f:
            rows.append(line.strip().split(','))
    return rows

all_rows = parse_logs_bad('/logs/2026-05-24.csv')
errors = [r for r in all_rows if r[3] == 'ERROR']
print(f"Found {len(errors)} errors")

النتيجة على سيرفر Hetzner CCX23 بـ 32GB RAM: MemoryError بعد 2 دقيقة و 24 ثانية. الـ process اتقتل بـ OOM Killer عند الـ 30 جيجا. اللوج بيقول "Killed".

الكود الصح - Generator

Python
def parse_logs_good(path):
    with open(path, 'r') as f:
        for line in f:
            yield line.strip().split(',')

errors_count = 0
for row in parse_logs_good('/logs/2026-05-24.csv'):
    if row[3] == 'ERROR':
        errors_count += 1
print(f"Found {errors_count} errors")

النتيجة على نفس السيرفر بالظبط: الـ memory peak = 12 ميجا، وقت التنفيذ = 3 دقايق و 48 ثانية. توفير 99.97% من الذاكرة بـ yield واحدة بدل append+return. الفرق في الوقت بسيط (24 ثانية زيادة) لأن الـ I/O هو الـ bottleneck الحقيقي مش الـ CPU.

Generator Expressions: الـ shortcut اللي مش الكل بيعرفها

زي ما عندك List Comprehension بـ [x*2 for x in lst]، عندك Generator Expression بنفس الشكل بس بقوسين () بدل []:

Python
# List comprehension - بيحجز ذاكرة لكل العناصر
squares_list = [x**2 for x in range(10_000_000)]
# tracemalloc: 412 MB peak

# Generator expression - بيحجز ذاكرة لعنصر واحد
squares_gen = (x**2 for x in range(10_000_000))
# tracemalloc: 0.8 KB peak

# الاتنين بيشتغلوا بنفس الطريقة في الـ loop
total = sum(squares_gen)  # نفس النتيجة، 0% memory overhead

القاعدة العملية: لو هتمرّ على البيانات مرة واحدة بس، استخدم Generator Expression. لو محتاج تعدي عليهم مرتين، استخدم List (لأن Generator بيـ exhaust ومش هترجع تاني).

Pipeline من 3 Generators متربطين

الجمال الحقيقي إنك تربط Generators في pipeline. كل واحد بيستهلك من اللي قبله بدون ما الذاكرة تتراكم في أي طبقة:

Python
import json

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line

def parse_json(lines):
    for line in lines:
        try:
            yield json.loads(line)
        except json.JSONDecodeError:
            continue

def filter_errors(records):
    for r in records:
        if r.get('level') == 'ERROR':
            yield r

# Pipeline - كل العمليات بتشتغل lazily
pipeline = filter_errors(
    parse_json(
        read_lines('logs.jsonl')
    )
)

for error in pipeline:
    send_to_alerting(error)

على ملف 18 جيجا فيه 84 مليون سطر JSON: الذاكرة المستخدمة عمرها ما عدّت 14 ميجا، رغم إن الـ pipeline بيمر بـ 3 طبقات تحويل.

الـ Trade-offs: كل اختيار له ثمنه

  • المرور مرة واحدة فقط: Generator بيتـ exhaust بعد الـ loop الأول. لو محتاج تمر مرتين، يا إما تحفظ النتيجة في list (وتفقد ميزة الذاكرة)، يا إما تنشئ Generator جديد، يا إما تستخدم itertools.tee (اللي بيـ buffer العناصر داخلياً).
  • صعوبة الـ debugging: مينفعش تطبع gen بـ print(gen) وتشوف العناصر. لازم تحوّله مؤقتاً بـ list(gen) أو تستخدم itertools.islice لمعاينة أول N عنصر بدون استهلاك الباقي.
  • الـ random access مش موجود: gen[1000] هيرمي TypeError. لو محتاج تقفز لعنصر معين بترتيبه، Generator مش الحل أصلاً.
  • الـ overhead في الـ calls: كل yield فيه context switch صغير. لو الـ function بسيطة جداً والـ collection صغيرة (أقل من 1000 عنصر)، List comprehension بيكون أسرع بـ 8-12% حسب قياسات Python 3.13.

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

Generators مش الحل الصح في 4 حالات محددة:

  1. البيانات صغيرة (أقل من 10MB): الفرق في الذاكرة مش هيتلاحظ، وعقبة الـ debugging أكبر من المكسب. خلّيك على List.
  2. محتاج تمر على نفس البيانات أكتر من مرة: هتعمل الشغل مرتين بدل ما تخزّنه. الـ trade-off ضدك.
  3. الـ API بتاعتك بتطلب list صريحة: مكتبات زي pandas.DataFrame() أو numpy.array() هتـ consume الـ generator كله في الذاكرة فوراً، فمفيش فايدة من الـ laziness أصلاً. استخدم pd.read_csv مع chunksize بدلاً منه.
  4. محتاج تعرف الـ length مقدماً: len(gen) مش هيشتغل ويرمي TypeError. لو محتاج progress bar مع الحجم الكلي (زي tqdm)، استخدم list أو احسب الحجم بشكل منفصل قبل البدء.

الافتراضات اللي بنيت عليها الأرقام

كل القياسات أعلاه على Python 3.13.0، Linux kernel 6.6، Hetzner CCX23 (16 vCPU، 32GB RAM، NVMe SSD). على hardware أبطأ في الـ I/O (HDD مثلاً) الفرق في الوقت ممكن يكون أكبر. الفرق في الذاكرة بيفضل ثابت تقريباً لأنه مرتبط بحجم الـ Python objects مش بالـ disk.

المصادر والمراجع

  • PEP 255 - Simple Generators (Magnus Lie Hetland, 2001): القاعدة الأصلية لـ yield في Python، رابط: peps.python.org/pep-0255
  • PEP 380 - Syntax for Delegating to a Subgenerator (Greg Ewing, 2009): إضافة yield from لربط Generators، رابط: peps.python.org/pep-0380
  • توثيق Python 3.13 الرسمي - Yield Expressions: docs.python.org/3/reference/expressions.html#yield-expressions
  • توثيق itertools الرسمي - أدوات لازمة مع Generators (tee, islice, chain): docs.python.org/3/library/itertools.html
  • القياسات بـ tracemalloc.get_traced_memory() من المكتبة القياسية، ومراقبة الذاكرة بـ /usr/bin/time -v لـ Maximum resident set size.

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

افتح أكبر سكربت Python في مشروعك دلوقتي وابحث عن append() داخل loop بيقرا من ملف أو DB أو API. غيّر الـ function لتستخدم yield بدل append+return، وقيس الذاكرة قبل وبعد بـ tracemalloc.get_traced_memory(). لو الفرق أكتر من 50%، انت لقيت bottleneck حقيقي ووفّرت ساعات debugging مستقبلية. لو الفرق أقل من 10%، الـ generator مش الحل عندك — البيانات صغيرة والـ List أنسب.

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

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

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