أساسيات

المولّدات في Python: yield والمكرّرات ومولّدات القوائم والقواميس

آخر تحديث: · Skillaria.Top

المولّد في Python كائن يعطي القيم واحدة تلو الأخرى وعند الطلب فقط، دون أن يحفظها كلها في الذاكرة، ويُنشأ بدالة فيها الكلمة المفتاحية yield أو بتعبير مولّد بين أقواس دائرية. أما مولّدات القوائم والقواميس والمجموعات فتبني مجموعة جاهزة في سطر واحد، والشرط الذي يفلتر العناصر يُكتب في آخر التعبير، والشرط الذي يختار قيمة يُكتب في أوله.

المولّد في Python كائن يعطي القيم واحدة تلو الأخرى، ولا يعطيها إلا عندما تُطلب، ولا يحتفظ بالتسلسل كله في الذاكرة. يُنشأ بطريقتين: دالة فيها الكلمة المفتاحية yield، أو تعبير مولّد بين أقواس دائرية. وإلى جانبهما توجد «مولّدات القوائم» و«القواميس» و«المجموعات»، وهي صيغة مختصرة لبناء مجموعة جاهزة في سطر واحد. فيما يلي شرح لطريقة عمل python yield، والفرق بين المولّد والمكرّر، وكيف تتجنب أن تتحول مولّدات القوائم إلى سطر غير مقروء.

المكرّرات: ما الذي تقوم عليه حلقة for

لفهم المولّدات يجب أولاً فهم المكرّرات. الكائن القابل للتكرار (iterable) هو ما يمكن المرور عليه في حلقة: قائمة أو نص أو قاموس أو ملف. أما المكرّر (iterator) فهو كائن يتذكر موقعه الحالي، ويعطي العنصر التالي عند استدعاء next()، فإذا انتهت العناصر رفع StopIteration.

nums = [10, 20, 30]
it = iter(nums)      # الحصول على مكرّر من القائمة
print(next(it))      # 10
print(next(it))      # 20
print(next(it))      # 30
next(it)             # StopIteration

حلقة for تفعل هذا بالضبط: تستدعي iter() ثم next() حتى StopIteration. ويمكن كتابة مكرّر خاص بك كصنف فيه الطريقتان __iter__ و__next__:

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

print(list(Countdown(3)))  # [3, 2, 1]

هذا يعمل، لكنه كثير الكود المتكرر. والمولّدات تفعل الشيء نفسه في ثلاثة أسطر.

ما هو المولّد في Python وكيف يعمل yield

دالة المولّد دالة عادية فيها yield. استدعاؤها لا ينفّذ الكود، بل يعيد كائن مولّد. كل استدعاء لـ next() يشغّل الدالة حتى أقرب yield، فيعطي القيمة ويجمّد الدالة مع متغيراتها المحلية. والاستدعاء التالي يكمل من الموضع نفسه بالضبط.

def countdown(start):
    while start > 0:
        yield start
        start -= 1

gen = countdown(3)
print(gen)          # <generator object countdown at 0x...>
print(next(gen))    # 3
print(list(gen))    # [2, 1] — استأنفنا من موضع التوقف

عندما تصل الدالة إلى نهايتها (أو إلى return) يرفع المولّد StopIteration بنفسه، وتنتهي حلقة for بهدوء. وهنا الفرق الأساسي بين yield وreturn: الأولى توقف الدالة مؤقتاً وقد تعطي قيماً كثيرة، والثانية تنهي الدالة وتعيد قيمة واحدة.

المولّد والمكرّر: الفروق

كل مولّد مكرّر، لأن له الطريقتين __iter__ و__next__. لكن ليس كل مكرّر مولّداً. والفرق في طريقة الإنشاء وفي الإمكانات.

مكرّر (صنف)مولّد
طريقة الإنشاءصنف فيه __iter__ و__next__دالة فيها yield أو (x for x in ...)
الحالةتُحفظ يدوياً في الخصائصتُحفظ تلقائياً في المتغيرات المحلية
StopIterationترفعه بنفسكيُرفع تلقائياً عند الخروج من الدالة
إمكانات إضافيةما تكتبه أنتsend() وthrow() وclose()

الخاصية المشتركة المهمة بينهما: يمكن المرور عليهما مرة واحدة فقط. تكرار for على مولّد انتهى لن يعطي شيئاً، ويجب إنشاء مولّد جديد.

مولّد لا نهائي وقراءة الملفات الكبيرة

المولّد لا يجب أن ينتهي. التسلسل اللانهائي آمن ما دمت تأخذ منه عدداً محدوداً من العناصر:

from itertools import islice

def ids():
    n = 1
    while True:
        yield f"ID-{n:04d}"
        n += 1

print(list(islice(ids(), 3)))  # ['ID-0001', 'ID-0002', 'ID-0003']

أكثر الاستخدامات عملية هو معالجة ملف كبير سطراً سطراً. يمكن ربط المولّدات في سلسلة، فيكون في الذاكرة في كل لحظة سطر واحد، لا الملف كله:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

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

for line in only_errors(read_lines("app.log")):
    print(line)

yield from وsend

yield from تمرر كل القيم من كائن قابل للتكرار آخر، فتستغني عن الحلقة المتداخلة:

def flatten(groups):
    for g in groups:
        yield from g

print(list(flatten([[1, 2], [3], [4, 5]])))  # [1, 2, 3, 4, 5]

الطريقة send() ترسل قيمة إلى داخل المولّد، فتصبح نتيجة تعبير yield. قبل أول send يجب «تشغيل» المولّد بـ next(). وبهذا يُكتب مثلاً متوسط متراكم:

def averager():
    total, count = 0, 0
    avg = None
    while True:
        value = yield avg
        total += value
        count += 1
        avg = total / count

a = averager()
next(a)            # الوصول إلى أول yield
print(a.send(10))  # 10.0
print(a.send(20))  # 15.0

في الكود التطبيقي يُستخدم send نادراً، لكنه يُسأل عنه كثيراً في المقابلات.

هل range مولّد؟

لا. range متتالية كسولة: هو أيضاً لا يحفظ كل الأرقام، لكنه يدعم الفهرسة وlen() والتحقق بـ in، ويمكن المرور عليه أكثر من مرة. والمولّد لا يفعل أياً من ذلك.

r = range(5)
print(r[2], len(r), 3 in r)  # 2 5 True
print(list(r), list(r))      # في المرتين [0, 1, 2, 3, 4]

مولّد القوائم: list comprehension

مولّد القوائم (list comprehension) هو الصيغة [تعبير for x in مجموعة]. تنشئ قائمة جديدة ولا تعدّل الأصلية. تُقرأ من اليسار إلى اليمين: أولاً «ماذا نضع»، ثم «من أين نأخذ».

amounts = [100, 250, 400]

# حلقة عادية
with_markup = []
for a in amounts:
    with_markup.append(a * 1.2)

# الشيء نفسه بمولّد القوائم
with_markup = [a * 1.2 for a in amounts]
print(with_markup)  # [120.0, 300.0, 480.0]

رغم أن اسمه «مولّد القوائم»، فالناتج قائمة جاهزة لا مولّد، إذ تُحسب كل العناصر فوراً.

مولّد قائمة بشرط if وif else

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

amounts = [100, 250, 400]

big = [a for a in amounts if a > 200]                   # [250, 400]
labels = ["كثير" if a > 200 else "قليل" for a in amounts]
# ['قليل', 'كثير', 'كثير']

# الفلترة والاختيار معاً
odd_marks = ["زوجي" if n % 2 == 0 else "فردي" for n in range(6) if n > 2]
# ['فردي', 'زوجي', 'فردي']

الصيغة [a for a in amounts if a > 200 else 0] خاطئة نحوياً: else تأتي فقط مع الشرط الذي في البداية.

مولّدات القوائم المتداخلة والقائمة الثنائية

الحلقة المزدوجة تُكتب بالترتيب نفسه للحلقات المتداخلة، من الأعلى إلى الأسفل. وهذا يساعد على عدم الخلط في ترتيب دمج قائمة من القوائم:

groups = [[1, 2], [3], [4, 5]]
flat = [x for g in groups for x in g]
# يعادل:
# for g in groups:
#     for x in g:
#         flat.append(x)
print(flat)  # [1, 2, 3, 4, 5]

القائمة الثنائية (المصفوفة) تُنشأ بمولّد داخل مولّد. والخطأ الكلاسيكي هنا ضرب القائمة: الصفوف تصبح الكائن نفسه.

bad = [[0] * 3] * 2
bad[0][0] = 1
print(bad)   # [[1, 0, 0], [1, 0, 0]] — الصفان تغيرا معاً

grid = [[0] * 3 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0, 0], [0, 0, 0]]

# التبديل بين الصفوف والأعمدة
m = [[1, 2, 3], [4, 5, 6]]
print([[row[i] for row in m] for i in range(3)])  # [[1, 4], [2, 5], [3, 6]]

مولّد القواميس والمجموعات

الطريقة نفسها تعمل مع المجموعات الأخرى. الأقواس المعقوفة مع نقطتين تبني قاموساً (dict comprehension)، والأقواس المعقوفة بدون نقطتين تبني مجموعة (set comprehension).

spent = [("طعام", 500), ("تاكسي", 300), ("طعام", 200)]

categories = {cat for cat, _ in spent}
print(categories)  # {'طعام', 'تاكسي'} — لا ضمان للترتيب في المجموعة

last = {cat: amount for cat, amount in spent}
print(last)        # {'طعام': 200, 'تاكسي': 300}

squares = {n: n ** 2 for n in range(4)}  # {0: 0, 1: 1, 2: 4, 3: 9}
swapped = {v: k for k, v in squares.items()}

انتبه إلى المتغير last: عند تكرار المفتاح تبقى القيمة الأخيرة، فقُدر المبلغ 500 وضاع. إذا أردت الجمع فلا يناسب مولّد القواميس، بل اكتب حلقة أو استخدم collections.Counter أو defaultdict.

ماذا عن مولّد الصفوف؟

لا يوجد «مولّد صفوف» مستقل في Python. الأقواس الدائرية تعطي تعبيراً مولّداً، والصف يُنشأ بتمريره إلى tuple():

t = tuple(n * 2 for n in range(3))  # (0, 2, 4)

التعبير المولّد: لا تخزّن بل أعطِ

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

import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))
print(sys.getsizeof(as_list))  # نحو 8 ميغابايت
print(sys.getsizeof(as_gen))   # بضع مئات من البايتات

تعيش التعابير المولّدة غالباً داخل دوال التجميع. وإذا كان المولّد هو المعامل الوحيد فلا حاجة إلى الأقواس الإضافية. والدالتان any() وall() تتوقفان عند أول إجابة ولا تكملان المرور على الباقي.

spent = [("طعام", 500), ("تاكسي", 300), ("طعام", 200)]

total = sum(a for _, a in spent)            # 1000
has_big = any(a > 400 for _, a in spent)    # True، ولا يتابع بعد 500
biggest = max(spent, key=lambda p: p[1])    # ('طعام', 500)

قاعدة الاختيار بسيطة: إذا احتجت النتيجة أكثر من مرة، أو الفهرسة، أو len()، فاختر القائمة. وإذا أردت المرور مرة واحدة وحساب شيء ما، فاختر المولّد.

نطاق المتغيرات: المتغير لا يتسرب

سؤال يتعثر فيه حتى المتمرسون:

x = 10
squares = [x for x in range(3)]
print(x)  # 10

متغير الحلقة داخل مولّد القوائم يعيش في نطاقه الخاص، ولا يمحو المتغير الخارجي (هكذا في Python 3، أما في Python 2 فكان الأمر مختلفاً). وفي حلقة for العادية كان x سيساوي 2 بعدها. ومنذ الإصدار 3.12 (PEP 709) تُدمج مولّدات القوائم في الدالة المحيطة وتعمل أسرع، لكن قاعدة الرؤية بقيت كما هي.

فك التعبئة * و** داخل المولّدات: جديد في Python 3.15 (PEP 798)

النجمة لفك التعبئة مسموحة منذ زمن في استدعاء الدالة وفي القائمة الحرفية، أما داخل المولّد فكانت SyntaxError. في Python 3.15 أُزيل المنع: *عنصر تعني «ضع هنا كل عناصره». وتعمل في القوائم والمجموعات والمولّدات، بما فيها المولّدات غير المتزامنة.

# Python 3.15+
groups = [[1, 2], [3], [4, 5]]
flat = [*g for g in groups]        # [1, 2, 3, 4, 5]
uniq = {*g for g in groups}        # مجموعة
lazy = (*g for g in groups)        # مولّد كسول

النجمتان المزدوجتان تفعلان الشيء نفسه مع القواميس. وهذا مفيد للإعدادات الطبقية «الافتراضي ← الملف ← الأعلام»، حيث تغطي كل طبقة المفاتيح التي قبلها.

# Python 3.15+
layers = [{"debug": False, "port": 80}, {"port": 8080}, {"debug": True}]
config = {**layer for layer in layers}
# {'debug': True, 'port': 8080}

ملاحظتان من مادة الدورة. لا تجمع النجمة مع التعبير الشرطي بدون أقواس: [*w if w else 0 for w in weeks] خطأ نحوي، ويجب وضع الشرط بين أقواس. وهذا الكود لن يعمل على الإصدار 3.14 وما قبله: إذا كانت المكتبة تدعم الإصدارات القديمة فاستخدم الحلقة المزدوجة for أو itertools.chain.from_iterable.

المولّدات غير المتزامنة

إذا كانت الدالة async def تحتوي على yield فالناتج مولّد غير متزامن. يُمرَّر عليه بـ async for، ويمكن داخله انتظار الشبكة أو القرص بـ await. وتوجد أيضاً مولّدات قوائم غير متزامنة.

import asyncio

async def ticks(n):
    for i in range(n):
        await asyncio.sleep(0.1)
        yield i

async def main():
    result = [i async for i in ticks(3)]
    print(result)  # [0, 1, 2]

asyncio.run(main())

أخطاء شائعة

  • مولّد قائمة من ثلاثة طوابق. حلقتان، وشرطان، وتعبير شرطي في سطر واحد لن يقرأه أحد بعد أسبوع. إذا لم يتسع للسطر فاكتب حلقة عادية، فـ PEP 8 يضع الوضوح قبل الاختصار.
  • المرور الثاني على المولّد. الاستدعاء الثاني لـ sum(gen) سيعيد 0، لأن المولّد انتهى.
  • مولّد قائمة من أجل أثر جانبي. [print(x) for x in xs] تبني قائمة لا فائدة منها من قيم None. للإجراءات استخدم حلقة for.
  • توقع قائمة من تعبير مولّد. (x for x in xs)[0] تعطي TypeError، فالمولّد ليس له فهارس.

تمارين على المولّدات

  1. اكتب مولّد fib() يعطي أرقام فيبوناتشي بلا نهاية، وخذ أول 10 منها باستخدام islice.
  2. احصل في سطر واحد على قاموس «كلمة ← طولها» للكلمات التي طولها أكثر من ثلاثة أحرف.
  3. احسب مجموع مربعات الأعداد الزوجية حتى مليون، دون إنشاء قائمة.
  4. اكتب مولّد chunks(seq, n) يعطي أجزاء من القائمة طول كل منها n عنصراً.

الخلاصة

المكرّر يعطي العناصر واحداً تلو الآخر عبر next()، والمولّد أبسط طريقة لكتابته: دالة فيها yield في بايثون، أو تعبير بين أقواس دائرية. مولّدات القوائم والقواميس والمجموعات تبني مجموعة جاهزة في سطر واحد: شرط الفلترة if يوضع في النهاية، وشرط الاختيار if else يوضع في البداية. إذا أردت توفير الذاكرة أو معالجة تدفق بيانات مرة واحدة فاختر المولّد، وإذا احتجت النتيجة مرات عديدة فاختر القائمة. وقد شُرحت مولّدات التجميع والمولّدات وفك التعبئة الجديد في Python 3.15 عملياً في الدورة، من خلال متتبّع المصروفات.

الأسئلة الشائعة

هل يمكن استخدام return داخل مولّد؟

نعم. return بدون قيمة تنهي المولّد فقط، أما return مع قيمة فتُخزَّن في الخاصية value للاستثناء StopIteration، وهذه القيمة تحصل عليها تعبير yield from.

كيف أحول المولّد إلى قائمة؟

مرّر المولّد إلى list(): list(gen). وتذكّر أن المولّد ينتهي بعدها، وكل القيم تصبح في الذاكرة.

أيهما أسرع: مولّد القوائم أم حلقة مع append؟

عادةً يكون مولّد القوائم أسرع قليلاً، لأنه لا يستدعي الطريقة append في كل تكرار. لكن الاختيار يكون بحسب الوضوح، والفرق نادراً ما يكون حاسماً.

كيف أعرف أن الكائن مولّد؟

استخدم inspect.isgenerator(obj) للكائن أو inspect.isgeneratorfunction(f) للدالة، ولمعرفة أن الكائن مكرّر استخدم isinstance(obj, collections.abc.Iterator).

ما الفرق بين تعبير المولّد ومولّد القوائم؟

الأقواس المربعة تبني القائمة كلها في الذاكرة فوراً، أما الأقواس الدائرية فتنشئ مولّداً يحسب العناصر عند الطلب ولا يُمرَّر عليه إلا مرة واحدة.

تعلّم هذا الموضوع بالفيديو

يُشرح هذا الموضوع بالتفصيل مع التطبيق العملي في دورة «Python 3.15: от новичка до профи».

الانتقال إلى الدورة