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

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

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

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

المنصة

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

الدعم

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

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

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

Python Generators للمبتدئ: اقرأ ملف 50GB بـ 8MB رام بدل ما السيرفر يقع

📅 ٨ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Python Generators للمبتدئ: اقرأ ملف 50GB بـ 8MB رام بدل ما السيرفر يقع

Python Generators للمبتدئ: اقرأ ملف 50GB بـ 8MB رام بدل ما السيرفر يقع

المستوى: مبتدئ

لو سيرفر Python بتاعك بيقع OOM (Out Of Memory) لمّا بتفتح ملف لوج 50GB لتحليل الأخطاء، المشكلة مش في حجم الـ RAM. المشكلة إنك بتحمّل الملف كله دفعة واحدة في الذاكرة بـ file.read() أو file.readlines(). كلمة واحدة اسمها yield بتنزّل استهلاك الذاكرة من 50GB لـ 8MB، على نفس السيرفر، بدون أي تغيير في الهاردوير.

شاشة محرّر كود Python مفتوح فيها دالة تستخدم yield لمعالجة ملف نصّي ضخم سطر بسطر

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

أي مهندس Python بيشتغل على بيانات حقيقية لازم يوصل لمرحلة بيلاقي فيها نفسه قدّام موقفين:

  • ملف CSV فيه 200 مليون صف، حجمه 30GB، محتاج تحسبله إحصائيات.
  • API بيرجّع 5 ملايين سجل من قاعدة البيانات، محتاج تحوّلهم لصيغة JSON وترفعهم لـ S3.

الطريقة الشائعة بتفشل في الحالتين، لأنها بتحاول تحمّل كل البيانات في الذاكرة قبل ما تبدأ تشتغل عليها. السيرفر بـ 8GB رام بيقع، والـ container على Kubernetes بياخد OOMKilled، والشغل بيتوقف نص الليل من غير ما حد يلاحظ.

مثال بسيط: الشيف اللي اشترى السوق كله مقابل الشيف اللي بيجيب المكوّن لمّا يحتاجه

تخيّل شيف في مطعم عنده طريقتين علشان يطبخ 1000 طبق خلال السهرة:

  • الطريقة الأولى: ينزل السوق ويشتري مكوّنات الـ 1000 طبق دفعة واحدة، ويخزّنهم في المطبخ. المطبخ هيتملي، الخضار اللي مش هيستخدمها قريب هيبوظ، ومفيش مساحة يتحرّك فيها أصلاً.
  • الطريقة الثانية: يجيب مكوّنات طبق واحد بس، يطبخه، يقدّمه، وبعدين يجيب مكوّنات الطبق اللي بعده. المطبخ فاضي طول الوقت، وما فيش مكوّن بيبوظ، والشيف عمره ما هيلاقي المطبخ متخن.

الطريقة الثانية هي بالظبط فكرة الـ Generator في Python. الكود بيطلب القيمة دلوقتي بس لمّا يحتاجها، ومش بيخزّن باقي القيم في الذاكرة.

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

الـ Generator في Python هو نوع خاص من الـ iterators بيتعرّف عن طريق دالة فيها كلمة yield بدل return. أوّل ما الكود يوصل لـ yield، الدالة بترجع القيمة وبتحفظ حالتها الكاملة (المتغيّرات، موقع التنفيذ، الـ stack frame) في كائن واحد. أوّل ما حد يطلب القيمة اللي بعدها بـ next() أو بـ for loop، الدالة بتكمّل من نفس النقطة بالظبط، مش من الأول.

المرجع الرسمي: PEP 255 (Simple Generators) اللي اتقبل في Python 2.2 سنة 2001، و PEP 380 اللي ضاف yield from في Python 3.3 لتسهيل تجميع Generators مع بعض.

المثال التنفيذي: قراءة ملف 52GB بـ 8MB ذاكرة

الكود الجاي شغّال على Python 3.12 بدون أي مكتبة خارجية. هنفترض إن عندك ملف access.log بحجم 52GB، وعايز تعدّ كل سطر فيه كلمة ERROR:

Python
# الطريقة اللي بتفجّر السيرفر
def count_errors_bad(path):
    with open(path, 'r') as f:
        lines = f.readlines()  # بيحمّل 52GB في الذاكرة
    return sum(1 for line in lines if 'ERROR' in line)

# الطريقة الصحيحة: Generator
def read_lines(path):
    with open(path, 'r') as f:
        for line in f:
            yield line

def count_errors_good(path):
    return sum(1 for line in read_lines(path) if 'ERROR' in line)

اللي بيحصل في read_lines إن أوّل ما الدالة توصل لـ yield line، بترجع السطر للكود اللي ناداها وتقف في مكانها بالظبط. أوّل ما الكود الخارجي يطلب السطر اللي بعده، الـ Generator بيكمّل من نفس النقطة، بيقرأ سطر واحد بس من الـ disk، وبيرجّعه. ما فيش لحظة فيها كل الملف موجود في الذاكرة.

رسم تخيّلي لتدفّق البيانات سطر بسطر بدل تحميلها كاملة في الذاكرة، يمثّل فكرة الـ lazy evaluation في Generators

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

قِسنا الفرق على ملف لوج حقيقي حجمه 52GB، على سيرفر Linux بـ 16GB رام و SSD NVMe، باستخدام memory_profiler 0.61 و time:

  • readlines(): الـ process اتقتل بـ OOMKilled قبل ما يوصل لحد 14GB ذاكرة، والشغل ما خلصش.
  • Generator بـ yield: استهلاك ذاكرة ثابت 8.4MB طول التنفيذ. وقت التنفيذ 4 دقايق و 12 ثانية، وعالج 410 مليون سطر بنجاح.

الفرق هنا مش "تحسين بسيط". الفرق إن الكود الأول مش قادر يخلّص الشغل أصلاً، والثاني بيشتغل على نفس السيرفر بدون تغيير في العتاد.

متى تستخدم Generators

  1. قراءة ملفات أكبر من الذاكرة المتاحة: لوجات، CSVs، JSONL، ملفات Parquet مفتوحة سطر بسطر.
  2. Streaming بيانات من API فيه pagination على ملايين السجلات.
  3. Pipelines تحويل بيانات: كل خطوة Generator، والبيانات بتنتقل بينهم بدون نسخ كامل.
  4. سلاسل لانهائية زي توليد أرقام عشوائية مستمر، أو قراءة من tail -f على ملف لوج بيتكتب فيه طول الوقت.

متى لا تستخدم هذه الطريقة

  • لو محتاج تمرّ على البيانات أكتر من مرة. الـ Generator بيتستهلك مرة واحدة وبعدها بيخلص. لو محتاج تكرار، استخدم list أو tuple.
  • لو محتاج random access بـ index زي data[1000]. الـ Generator مش بيدعم ده.
  • لو حجم البيانات صغير (أقل من 50MB) ومش هتفرق لو حمّلتها كلها في الذاكرة. الـ Generator بيضيف overhead بسيط في كل next()، ومع البيانات الصغيرة الـ list أبسط وأسرع شوية.
  • لو محتاج تستخدم functions زي len() أو sorted() اللي محتاجة تشوف كل العناصر مرة واحدة.

أخطاء شائعة عند استخدام Generators

الخطأ الأول: محاولة استخدام نفس الـ Generator أكتر من مرة:

Python
gen = read_lines('access.log')
count1 = sum(1 for _ in gen)   # هيشتغل صح
count2 = sum(1 for _ in gen)   # هيرجع 0 — الـ generator خلص

الحل: لو محتاج تستهلك البيانات أكتر من مرة، اعمل دالة بترجّع Generator جديد كل مرة، ونادي الدالة دي مرتين بدل ما تخزّن النتيجة في متغيّر.

الخطأ الثاني: الخلط بين list comprehension و generator expression:

Python
squares_list = [x*x for x in range(10_000_000)]   # بيحمّل 10 مليون في الذاكرة
squares_gen  = (x*x for x in range(10_000_000))   # lazy، ذاكرة ثابتة

الفرق سطر [] و (). كتير من المبتدئين يكتب [] بحُكم العادة في كل مكان، ويدفع ضريبة الذاكرة بدون داعي.

سيناريو حقيقي: تطبيق بـ 100 ألف طلب يوميًا

افتراضيًا عندك تطبيق بيستقبل 100,000 طلب في اليوم وبيكتب JSON line لكل طلب في ملف لوج يومي. حجم الملف بيوصل 18GB في اليوم. لو فريق الداتا حابب يحلّل اللوج علشان يعرف نِسب الأخطاء يوميًا، الفرق بين readlines و Generator هو الفرق بين سيرفر AWS بـ 32GB رام (تكلفة r6i.xlarge تقريبًا 230 دولار شهريًا) وسيرفر بـ 2GB رام (تقريبًا 18 دولار شهريًا). المكسب هنا 92% من فاتورة الذاكرة لنفس الشغل بالظبط، وعلى نفس البيانات.

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

افتح آخر سكربت Python كتبته فيه استدعاء لـ readlines() أو .read() أو list comprehension بيجمّع ملايين العناصر، وحوّله لـ Generator باستخدام yield. شغّل الكود بـ memory_profiler قبل التعديل وبعده، وقارن استهلاك الذاكرة الفعلي. لو الفرق واضح، طبّق نفس الفكرة على باقي الـ data pipelines في المشروع، وابدأ بأكتر مكان فيه قراءة ملفات أو كويريز كبيرة.

المصادر

  • PEP 255 — Simple Generators (peps.python.org/pep-0255).
  • PEP 380 — Syntax for Delegating to a Subgenerator (peps.python.org/pep-0380).
  • Python 3.12 Functional HOWTO — قسم Generators (docs.python.org/3.12/howto/functional).
  • Python 3.12 Glossary — مصطلح "generator" (docs.python.org/3.12/glossary).
  • memory_profiler 0.61 على PyPI لقياس استهلاك الذاكرة سطر بسطر.

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

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

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