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

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

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

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

المنصة

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

الدعم

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

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

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

Python Generators: ليه range(10**8) مش بيفجر الذاكرة؟

📅 ١٩ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Python Generators: ليه range(10**8) مش بيفجر الذاكرة؟

لو عندك ملف CSV بحجم 5 جيجابايت وفتحته بطريقة ساذجة زي f.readlines()، البرنامج بياكل RAM ويوقع. الحل اللي بايثون بيقدمه ببلاش اسمه generators، وهو مش مجرد syntactic sugar. ده بياخد نموذج التنفيذ من eager لـ lazy. هنا هتفهم بالظبط امتى تستخدمه، امتى لا، وإزاي تبني واحد فعلاً شغال في production.

Python Generators بالتفاصيل: من yield لحد الـ streaming pipelines

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

لما تكتب list(range(10**8)) بايثون بيحجز ~3.3 جيجابايت ذاكرة علشان يخزّن 100 مليون رقم صحيح. بس لو كتبت range(10**8) من غير list(...)، الذاكرة المستهلكة تقريبًا 48 بايت ثابتة. الفرق مش في الأرقام، الفرق في متى القيم بتتحسب. ده بالظبط اللي generators بتعمله.

شاشة محرر كود تعرض سطور بايثون مع تمييز كلمة yield

eager vs lazy: الفرق اللي بيغير شكل الكود

الـ list في بايثون eager: بتتحسب كل عناصرها وبتتخزن في الذاكرة وقت الإنشاء. الـ generator lazy: القيم بتتولّد بس لما حد يطلبها بـ next() أو بـ for loop. ده بيديك ميزتين:

  • استهلاك ذاكرة ثابت تقريبًا، بغض النظر عن حجم البيانات الكلي.
  • القدرة على تمثيل سلاسل لا نهائية (infinite streams) من غير ما البرنامج يقع.

إزاي تكتب generator: yield بدل return

أي function فيها كلمة yield بقت generator. ما بتتنفّذش لما تستدعيها، بترجع generator object بتتحرك فيه خطوة بخطوة، وكل مرة بيوصل لـ yield بيوقف ويستنى الاستدعاء التالي.

Python

def read_large_csv(path):
    with open(path, "r", encoding="utf-8") as f:
        header = next(f).strip().split(",")
        for line in f:
            values = line.strip().split(",")
            yield dict(zip(header, values))

# الاستخدام: بيقرا سطر سطر، مش الملف كامل
for row in read_large_csv("events_5gb.csv"):
    if row["status"] == "error":
        print(row["user_id"], row["message"])

الكود ده بيشتغل على ملف 5 جيجا بذاكرة تقريبًا 12 ميجا ثابتة خلال كل التنفيذ. الفرق الجوهري: الملف ما بيتحملش في RAM، بيتقرا streaming من الـ disk سطر بسطر.

generator expressions: lazy list comprehension

بدل list comprehension بـ []، استخدم () وهتلاقي نفس المنطق بس lazy:

Python

# eager — بيولّد 10 مليون قيمة قبل ما يجمع
total = sum([x * x for x in range(10_000_000)])

# lazy — بيولّد قيمة، بيجمعها، بينسى، ويكمل
total = sum(x * x for x in range(10_000_000))

الاتنين بيرجّعوا نفس النتيجة، بس الأول محتاج ~400MB، التاني محتاج أقل من 1KB. في الـ hot path، الاختيار الصح بيوفر RAM كتير بدون أي تغيير حقيقي في شكل الكود.

سيناريو واقعي: تحليل logs على سيرفر إنتاج

الافتراض: عندك سيرفر ويب بيستقبل 200K طلب/يوم، وكل طلب بيسجل سطر في access.log. حجم الملف الأسبوعي ~3GB. عايز تحسب عدد الطلبات اللي status code بتاعها ≥500 في آخر 7 أيام.

Python

import re

pattern = re.compile(r'"\s(\d{3})\s')

def status_codes(log_path):
    with open(log_path, "r") as f:
        for line in f:
            match = pattern.search(line)
            if match:
                yield int(match.group(1))

def count_errors(log_path):
    return sum(1 for code in status_codes(log_path) if code >= 500)

print(count_errors("/var/log/nginx/access.log"))

النتيجة بالأرقام على ملف 3GB حقيقي (قياس تقديري على SSD عادي):

  • باستخدام f.readlines(): ~3.1GB RAM، وقت التنفيذ ~22 ثانية.
  • باستخدام generator: ~9MB RAM، وقت التنفيذ ~18 ثانية.

الأسرع والأخف في نفس الوقت. السبب: الـ generator بيتعامل مع الـ I/O بشكل متداخل مع المعالجة، فالـ CPU ما بيفضلش مستنّي الملف يتحمّل كله قبل ما يبدأ الشغل.

رسم تمثيلي لاستهلاك الذاكرة مقارنة بين list و generator في بايثون

Chaining: بناء pipeline كامل من generators

أفضل طريقة تستغل generators إنك تبني pipeline من generators متسلسلة. كل مرحلة بتاخد iterator وترجع iterator، ومفيش حاجة بتتخزن في النص.

Python

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

def filter_errors(rows):
    for row in rows:
        if row[2].startswith("5"):
            yield row

def extract_user_ids(rows):
    for row in rows:
        yield row[0]

with open("access.log") as f:
    pipeline = extract_user_ids(filter_errors(parse_lines(f)))
    unique_users = set(pipeline)

print(len(unique_users))

كل مرحلة من التلاتة لسه lazy. الـ set في الآخر هو اللي فعلاً بيستهلك الـ generator ويجبر التنفيذ. ده النموذج اللي مكتبات زي itertools و more-itertools بتستغله، وهو أساس أدوات data processing زي Apache Beam و Ray.

الـ trade-offs: اللي بتخسره مقابل الذاكرة

  • مش بتقدر ترجع للخلف. generator one-pass. لو محتاج تعمل عليه loop مرتين، لازم تنشئه من جديد أو تحوّله لـ list.
  • مفيش len(). ما بتعرفش كام عنصر جوه قبل ما تستهلكه كله.
  • Debugging أصعب. الـ stack trace لما يقع بيحصل في اللحظة اللي بتستهلك فيها القيمة، مش لحظة تعريف الـ generator. ده بيربك كتير أول مرة.
  • الـ pickling محدود. مش كل الـ generators بتتخزن للـ serialization، فلو بتستخدم multiprocessing خلي بالك.

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

في حالات الـ list أو الـ tuple فعلاً أفضل:

  • البيانات صغيرة (أقل من عشرات الآلاف من العناصر). الـ overhead اللي generator بيضيفه في كل next() call بيطلع أكبر من الفائدة.
  • محتاج random access زي data[1000]. generators ما بتدعمش indexing.
  • محتاج sorted() أو reversed(). دي عمليات محتاجة كل العناصر في الذاكرة على الأقل مرة.
  • بتشارك البيانات بين كذا consumer في نفس الوقت. الـ generator الواحد ما ينفعش يتقسم؛ هتحتاج itertools.tee واللي ممكن يرجع الاستهلاك العالي تاني.

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

افتح أي script عندك بيقرا ملف أو بيعمل list comprehension على بيانات أكبر من 50K عنصر. غيّر [] لـ () في الـ comprehensions، وغيّر الـ functions اللي بترجع list لـ functions بـ yield. قيس الفرق بـ tracemalloc قبل وبعد. لو ما لقيتش فرق ملموس، المشكلة مش في الذاكرة، جرّب تعمل profiling للـ CPU بـ cProfile.

المصادر

  • Python Official Docs — Functional HOWTO / Generators: docs.python.org/3/howto/functional.html#generators
  • PEP 255 — Simple Generators: peps.python.org/pep-0255
  • PEP 289 — Generator Expressions: peps.python.org/pep-0289
  • David Beazley — Generator Tricks for Systems Programmers: dabeaz.com/generators
  • Python tracemalloc module docs: docs.python.org/3/library/tracemalloc.html
  • itertools standard library: docs.python.org/3/library/itertools.html

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

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

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