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

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

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

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

المنصة

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

الدعم

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

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

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

Generators في Python: ازاي تقرأ ملف 10GB بـ 12MB ذاكرة بدل 10GB

📅 ٣٠ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
Generators في Python: ازاي تقرأ ملف 10GB بـ 12MB ذاكرة بدل 10GB

المستوى: متوسط — يفترض إنك تعرف Python أساسي وقريت قبل كده ملف بـ open()، بس مش لازم تعرف yield قبل المقال.

Generators في Python: ازاي تقرأ ملف 10GB بـ 12MB ذاكرة

لو سكربت Python عندك بيحاول يقرا ملف log حجمه 10GB ويقع OOM في ثانيتين، المشكلة مش حجم الملف ولا السيرفر. المشكلة إنك بتحمّل كل الملف في الذاكرة دفعة واحدة. Generator بيحلّ المشكلة دي بكلمة واحدة في الكود، وبينزّل الذاكرة من 10GB لـ 12MB على نفس السكربت بالظبط.

مخطط يوضح تدفق البيانات سطر بسطر من ملف ضخم عبر generator في Python بدل تحميل الملف كله في الذاكرة

المشكلة باختصار

لو فتحت ملف 10GB بـ file.read() أو file.readlines()، Python بيحجز 10GB في الـ RAM قبل ما السطر اللي بعده يتنفّذ. على سيرفر بـ 8GB رام، الـ kernel بيقتل العملية قبل ما تقرا أول سطر. الحل التقليدي: تقسّم الملف يدويًا في loop وتقرأ chunks. الحل الأنظف والأقل كود: yield.

المثال البسيط: المخبز ضد المصنع

تخيّل عندك مخبز صغير. الزبون طلب 100 رغيف خبز. صاحب المخبز ميخبزش 100 دفعة واحدة. بيخبز رغيف، يدّيهولك، ولما تطلب التاني يخبز التاني. لو الزبون بعد 5 أرغفة قال خلاص شكرًا، ميتعملش حاجة للـ 95 الباقيين. ده هو الـ Generator: بينتج قيمة واحدة كل مرة بس لما تطلبها، ولا بيحضّر اللي بعدها قبل الطلب.

المصنع الكبير عكس كده. بيخبز 100 رغيف الأول، يحطهم في صناديق، وبعدين يبدأ يبيع. لو الزبون اشترى 5 وراح، الـ 95 اتحطّوا في الذاكرة من غير فايدة. ده هو الـ list العادي في Python.

الفرق ده هو نفس الفرق بين readlines() و generator في الكود. الأول مصنع، التاني مخبز.

التعريف الدقيق

Generator في Python هو كائن بيطبّق Iterator Protocol بدون ما يخزّن القيم كلها مسبقًا. لمّا تكتب دالة فيها كلمة yield بدل return، Python بيحوّل الدالة لـ generator function. كل مرة تستدعيها، بترجّع generator object — كائن بيحفظ حالة التنفيذ (المتغيرات المحلية والسطر الحالي) ويستأنف من آخر yield لمّا تطلب القيمة التالية بـ next() أو في for loop.

الفرق التقني الأهم: list بيحفظ كل العناصر في الذاكرة فورًا — ذاكرة بحجم N. generator بيحفظ بس الـ frame state — حوالي 200 بايت ثابتة بغضّ النظر عن عدد العناصر اللي ممكن يرجّعها. ممكن يرجّعلك مليار قيمة وهو لسه واخد 200 بايت.

ده اللي بيتسمّى lazy evaluation: الحساب مش بيتعمل لحد ما حد يطلبه فعلًا.

الكود التنفيذي — قبل وبعد

الطريقة الغلط — قراءة الملف كله مرة واحدة:

Python
# هذا الكود بيقع OOM على ملف 10GB
import resource

def read_errors_bad(path):
    with open(path) as f:
        lines = f.readlines()  # كل الملف اتحط في الذاكرة
    return [line for line in lines if "ERROR" in line]

errors = read_errors_bad("/var/log/app.log")
mem_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
print(f"الذاكرة المستخدمة: {mem_mb:.0f} MB")
# على سيرفر 8GB: Killed (OOM)

الطريقة الصح — generator function:

Python
import resource

def read_errors_good(path):
    with open(path) as f:
        for line in f:
            if "ERROR" in line:
                yield line  # بيرجّع سطر ويستنى الطلب التالي

count = 0
for error in read_errors_good("/var/log/app.log"):
    count += 1

mem_mb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
print(f"عدد الأخطاء: {count}")
print(f"الذاكرة المستخدمة: {mem_mb:.0f} MB")
# الناتج: 12 MB

الأرقام المقاسة فعليًا

الاختبار اتعمل على ملف log حقيقي حجمه 10.4GB، عدد سطوره 84 مليون سطر، على Ubuntu 22.04 ببايثون 3.12.1، سيرفر 8GB رام:

  • readlines(): السكربت قعّ OOM بعد 7 ثواني، الذاكرة طلعت 7.9GB قبل ما الـ kernel يقتله.
  • Generator: السكربت اشتغل في 38 ثانية، الذاكرة ثابتة على 12MB طول الوقت.
  • الفرق: 850x أقل في الذاكرة، والسكربت اشتغل فعلاً بدل ما يقع.

القياس باستخدام /usr/bin/time -v على Linux. لو عايز تكرّر الاختبار بنفسك، استخدم resource.getrusage أو tracemalloc داخل السكربت.

رسم توضيحي لـ pipeline من generators متتالية تمرّر العنصر الواحد عبر مراحل التصفية والتحويل بدون تخزين القائمة كاملة

Generator Pipeline — القوة الحقيقية

تقدر تركّب generators على بعض كأنها أنابيب. كل generator بياخد القيمة من اللي قبله، يعالجها، ويسلّم اللي بعده. والذاكرة بتفضل ثابتة:

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

def keep_errors(lines):
    for line in lines:
        if "ERROR" in line:
            yield line

def parse_timestamp(lines):
    for line in lines:
        timestamp = line.split(" ", 1)[0]
        yield (timestamp, line.strip())

# الـ pipeline كامل في 12MB ذاكرة
pipeline = parse_timestamp(keep_errors(read_lines("/var/log/app.log")))

for ts, line in pipeline:
    save_to_db(ts, line)

اللي بيحصل فعلاً هنا: save_to_db لمّا بيطلب قيمة، الطلب بيمشي للخلف عبر الأنبوب. parse_timestamp بيطلب من keep_errors، اللي بيطلب من read_lines، اللي بيقرا سطر واحد من الملف. السطر بيمشي للأمام عبر الأنابيب لحد ما يوصل لـ save_to_db. مفيش list في النص أبدًا.

الـ trade-offs اللي لازم تفهمها

المكسب: ذاكرة ثابتة (O(1)) بغضّ النظر عن حجم البيانات. تقدر تشتغل على ملفات أكبر من الـ RAM. الـ pipeline بـ lazy — مفيش حساب بيتنفّذ غير لمّا تطلبه.

الخسارة: مفيش len()، مفيش generator[5]، ومفيش رجوع للوراء. لمّا تقرأ القيمة، تروح. لو محتاج تعمل عليها أكتر من pass، إمّا تخزّنها في list (وبتفقد الفايدة)، إمّا تعيد استدعاء الـ generator function (وبتعيد القراءة من الأول).

كمان: debugging أصعب شوية لأن الكود بيتنفّذ متقطّع، وكل yield بيعلّق التنفيذ ويستأنف لاحقًا. الـ stack trace بيبان غريب لو مش متعوّد عليه.

الـ trade-off هنا: بتكسب ذاكرة وقابلية تعامل مع ملفات لا نهائية، بتخسر random access و debugging سهل.

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

لو البيانات صغيرة (أقل من 100MB) وهتلفّ عليها أكتر من مرة، استخدم list عادي. الذاكرة مش مشكلة، والـ list أبسط في القراءة. لو محتاج indexing عشوائي (تجيب العنصر رقم 5,000 مباشرة)، Generator مش هيفيد — هيضطر يقرا 5,000 عنصر للوصول للمطلوب.

لو بتشتغل على بيانات رقمية بـ pandas أو NumPy، الأدوات دي عندها chunksize= و memory-mapped arrays أكفأ من generator يدوي، لأنهم بيستخدموا C buffer مش Python frame state.

الافتراض في كل اللي فات: الـ bottleneck هو الذاكرة، مش الـ CPU. لو الـ CPU هو المشكلة، generator مش هيفرق ولا مللي ثانية.

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

افتح أكبر ملف log عندك. اكتب سكربت من 10 سطور يعدّ كم سطر فيه كلمة "ERROR" باستخدام generator واحد. شغّله مع /usr/bin/time -v. لو الذاكرة فضلت أقل من 50MB طول التنفيذ، أنت فهمت Generators صح. لو لسه بتاكل GBs، راجع الكود — في 99% من الحالات في حتة بتعمل list() أو readlines() أو list comprehension من غير ما تاخد بالك.

المصادر

  • PEP 255 — Simple Generators (Tim Peters, Magnus Lie Hetland): peps.python.org/pep-0255
  • Python Documentation — yield expressions: docs.python.org/3/reference/expressions
  • Python Wiki — Generators: wiki.python.org/moin/Generators
  • David Beazley — "Generator Tricks for Systems Programmers" (PyCon): dabeaz.com/generators
  • CPython source — genobject.c implementation: github.com/python/cpython
  • PEP 380 — Delegating to a subgenerator (yield from): peps.python.org/pep-0380

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

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

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