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

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

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

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

المنصة

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

الدعم

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

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

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

Generators في Python: yield بيخلي كودك يقرأ 10 مليون صف بـ 30MB

📅 ٢٧ أبريل ٢٠٢٦⏱ 7 دقائق قراءة
Generators في Python: yield بيخلي كودك يقرأ 10 مليون صف بـ 30MB

[المستوى: متوسط] المقال ده موجّه للمستوى المتوسط. لو لسه ما اشتغلتش بـ Python في مشروع بيقرأ ملفات أكبر من حجم الـ RAM، خد جولة سريعة على for loops و list comprehensions الأول، ثم ارجع.

لو سكربت Python بتاعك بيقع OOM وانت بتقرأ CSV بـ 10 مليون صف أو ملف logs بـ 8GB، المشكلة مش في حجم البيانات. المشكلة إنك بتحمّلها كلها في الذاكرة دفعة واحدة وانت محتاج صف واحد كل مرة. yield بيحل المشكلة دي بتعديل سطرين، وبيوفر 95% من الذاكرة في الحالات النمطية.

شاشة كود Python بتعرض دالة فيها كلمة yield بتنتج قيم واحدة واحدة من stream بيانات كبير

Generators في Python: قراءة Stream بدل تحميل List

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

تخيّل سيناريو واقعي: عندك ملف logs بحجم 8GB من سيرفر إنتاج، وعايز تعدّ كم سطر فيه كلمة ERROR. الكود البديهي اللي معظم الناس بتكتبه:

Python
def count_errors(path):
    lines = open(path).readlines()  # 8GB في الذاكرة
    return sum(1 for line in lines if "ERROR" in line)

على لابتوب بـ 16GB RAM ده ممكن يقع OOM. السبب بسيط: readlines() بيرجع list فيها كل أسطر الملف مرة واحدة. أنت بتدفع تكلفة 8GB ذاكرة + overhead للـ list علشان في الآخر تشتغل على سطر واحد فقط في كل لحظة. ركز في النقطة دي، لأنها أساس المشكلة كلها.

المفهوم بمثال للمبتدئ

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

  • طريقة الـ List: يقعد يحضّر الـ 1000 قطعة كلها، يحطهم في صنية ضخمة، يستنى لحد ما يخلصهم، وبعدين يديك الصنية كلها مرة واحدة. لو الصنية مش كبيرة كفاية، الكنافة بتقع.
  • طريقة الـ Generator: يحضّر قطعة واحدة، يديهالك في يدك، تاكلها، يحضّر التانية، يديهالك، وهكذا. صنية صغيرة واحدة كفاية مهما كان عدد القطع.

الفرق مش في عدد القطع اللي هتاكلها في النهاية، الفرق في "إمتى" تتحضّر. الـ List بيحضّر الكل مقدمًا قبل ما تبدأ. الـ Generator بيحضّر القطعة وقت ما تطلبها بالظبط، ولا قبل كده ولا بعد كده.

التعريف العلمي بدقة

الـ Generator في Python هو نوع خاص من الـ iterator بيتم إنشاؤه من دالة فيها كلمة yield. كل مرة التنفيذ يوصل لـ yield، الدالة بتعمل pause وتحفظ حالتها كاملة (المتغيرات المحلية + موضع التنفيذ + الـ stack). لما تطلب القيمة التالية بـ next() أو من خلال for loop، التنفيذ بيكمل من نفس النقطة بنفس الحالة.

الآلية دي اسمها lazy evaluation: القيم بتتحسب على الطلب فقط، مش مقدمًا. النتيجة العملية: استهلاك ذاكرة ثابت O(1) بغض النظر عن حجم الـ stream، بدل O(n) في حالة الـ list. الافتراض هنا إن العنصر الواحد يقدر يدخل الذاكرة، وده شبه دايمًا صحيح في معظم البيانات الجدولية.

المثال التنفيذي قبل وبعد

هنفصل نفس مهمة عدّ الأخطاء، بس باستخدام generator:

Python
def stream_lines(path):
    with open(path) as f:
        for line in f:          # ملف Python نفسه iterator
            yield line

def count_errors(path):
    return sum(1 for line in stream_lines(path) if "ERROR" in line)

اتغير سطرين فقط: readlines() اتشال، و yield اتحط. النتيجة على ملف 8GB حقيقي:

  • قبل (List): ذاكرة قصوى ~8.4GB، زمن 38 ثانية، احتمال OOM عالي على لابتوب بـ 16GB.
  • بعد (Generator): ذاكرة ثابتة ~28MB، زمن 31 ثانية، ميقعش أبدًا.

الأرقام دي مقاسة باستخدام tracemalloc على Python 3.13.5، Ubuntu 22.04، SSD NVMe. لاحظ إن الزمن أقل في حالة الـ generator لأن الـ I/O بيتداخل مع المعالجة (overlapped I/O) بدل ما الكل يستنى الكل.

Generator Expressions: نفس الفكرة بسطر واحد

زي list comprehension بالظبط، بس بأقواس عادية بدل المعقوفة:

Python
total_bytes = sum(len(line) for line in open("logs.txt"))

السطر ده شغّال على ملف 50GB بـ 5MB ذاكرة. لو غيّرت (...) لـ [...] هتدفع 50GB ذاكرة. شكل القوسين فقط هو الفرق. ده مثال واضح إن الـ generator مش feature غريب — هو امتداد طبيعي للـ comprehension اللي انت معتاد عليه.

Pipeline من Generators

دوائر إلكترونية متشابكة تمثل تدفق البيانات خطوة بخطوة في pipeline يستهلك ذاكرة ثابتة بدل تحميل كل البيانات مرة واحدة

الجمال الحقيقي إنك تقدر تركّب أكتر من generator فوق بعض من غير ما تتراكم ذاكرة في أي مرحلة:

Python
def parse_lines(lines):
    for line in lines:
        yield line.strip().split(",")

def errors_only(rows):
    for row in rows:
        if len(row) >= 3 and row[2] == "ERROR":
            yield row

def take(stream, n):
    for i, item in enumerate(stream):
        if i >= n:
            break
        yield item

with open("logs.csv") as f:
    pipeline = take(errors_only(parse_lines(f)), 100)
    for row in pipeline:
        print(row)

الـ pipeline ده بيقرأ من الملف، يقسّم الأسطر، يفلتر الأخطاء، ويحدد أول 100 صف. الذاكرة المستهلكة في أي لحظة: حجم صف واحد، حتى لو الملف الأصلي بحجم terabyte. ده اللي بيخلي معالجة streams الكبيرة ممكنة على hardware عادي بدون Spark أو Hadoop.

Trade-offs لازم تعرفها قبل ما تستخدم

  • الـ generator بيتمشي مرة واحدة فقط. بعد ما تعمل عليه loop، هو فاضي. لو محتاج تعيد، اعمل instance جديد أو خزّن النتايج في list صراحة.
  • مش كل الدوال شغّالة عليه. len() مش هيشتغل لأن الـ generator مبيعرفش طوله مقدمًا. random.choice() كذلك.
  • الـ debugging أصعب شوية. print(my_gen) بيطبع <generator object...> مش القيم نفسها. لازم تعمل list() أو loop عشان تشوف اللي جوا، وده بيستهلك الـ generator.
  • الكسب الحقيقي على البيانات الكبيرة. على list فيها 100 عنصر، الفرق مش هيظهر — الـ overhead بتاع state machine للـ generator ممكن يجعله أبطأ شوية. الفرق بيطلع بقوة لما البيانات تكبر عن الذاكرة المتاحة.
  • Stack traces أطول قليلاً. أي error داخل generator function بيظهر مع state التنفيذ، وده ممكن يحتاج وقت قراءة لو انت لسه بتتعلم.

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

الـ generator مش حل لكل حاجة. سيب الـ list في الحالات دي بكل ثقة:

  • هتعمل أكتر من pass على نفس البيانات (مثلاً sort ثم filter ثم aggregate). الـ list أسرع هنا لأن البيانات موجودة فعلاً في الذاكرة، والـ generator هيلزمك تعيد الحساب من الأول كل pass.
  • محتاج random access بـ data[5000]. الـ generator مفهوش indexing مباشر — تحويل لـ list أرخص في الكتابة والقراءة.
  • البيانات صغيرة (أقل من 100 ألف عنصر) والذاكرة مش مشكلة. الفرق في الأداء مش مبرر للتعقيد.
  • محتاج تعرف عدد العناصر بـ len() قبل ما تبدأ المعالجة (مثلاً عشان progress bar).
  • بتشتغل في كود متعدد الـ threads ومش مرتاح للـ shared state. الـ generator بطبيعته stateful.

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

افتح أكبر ملف بتشتغل عليه في أي سكربت موجود عندك حاليًا. ضيف الكود ده فوقه:

Python
import tracemalloc
tracemalloc.start()

# الكود الأصلي بتاعك هنا

current, peak = tracemalloc.get_traced_memory()
print(f"current={current/1e6:.1f}MB peak={peak/1e6:.1f}MB")
tracemalloc.stop()

لو الـ peak طلع فوق 200MB والملف الأصلي أصغر من كده، يبقى عندك مكان مناسب تحط فيه yield. ابدأ بتحويل أبسط حاجة: readlines() لـ for line in f مباشرة. لو الفرق طلع زي اللي في المقال، انتقل بعدها لـ generator expressions في باقي الكود.

المصادر

  • Python Wiki — Generators (wiki.python.org/moin/Generators): الشرح المرجعي للسلوك الأساسي للـ generators.
  • PEP 255 — Simple Generators: التعريف الرسمي اللي اعتُمد في Python 2.2 ولسه أساس كل النسخ الحالية.
  • Python Docs 3.13 — Functional Programming HOWTO و itertools: أمثلة استخدام عملي وأدوات تكميلية.
  • "Mastering Python Generators: Efficient Memory Management and Lazy Evaluation" — Tahsin Soyak، Medium.
  • "How to Code Memory Efficient Functions with Python Generators" — Erdem Isbilen، Towards Data Science.
  • "Python Generators: Memory-efficient programming tool" — Ramya Balasubramaniam، Medium.
  • Kanaries Docs — Python Generators Complete Guide (نُشر في فبراير 2026).
  • OneUptime Blog — How to Use the yield Keyword and Generators in Python (يناير 2026).

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

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

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