Generator در پایتون چیست؟ آموزش Generators و yield

در این آموزش با Generator در پایتون آشنا می‌شویم و یاد می‌گیریم چگونه با استفاده از yield داده‌ها را به صورت مرحله‌ای تولید کنیم و مصرف حافظه را کاهش دهیم.

Generator در پایتون چیست؟

Generator یکی از قابلیت‌های مهم و کاربردی پایتون است که به ما اجازه می‌دهد مقادیر را یکی‌یکی و فقط زمانی که به آن‌ها نیاز داریم تولید کنیم.

برخلاف لیست‌ها که معمولاً تمام داده‌ها را یکجا در حافظه قرار می‌دهند، Generator می‌تواند داده‌ها را به صورت مرحله‌ای تولید کند.

این ویژگی مخصوصاً هنگام کار با حجم زیادی از داده‌ها اهمیت زیادی دارد.

چرا از Generator استفاده می‌کنیم؟

فرض کنید می‌خواهیم اعداد ۱ تا یک میلیون را پردازش کنیم. اگر همه این اعداد را در یک لیست ذخیره کنیم، حافظه قابل توجهی مصرف می‌شود.

اما Generator می‌تواند هر بار فقط یک مقدار را تولید کند. بنابراین لازم نیست تمام داده‌ها همزمان در حافظه قرار بگیرند.

  • مصرف حافظه کمتر
  • مناسب برای داده‌های حجیم
  • تولید مرحله‌ای داده‌ها
  • مناسب برای پردازش داده‌ها و فایل‌های بزرگ

دستور yield در پایتون

مهم‌ترین بخش Generator استفاده از کلمه کلیدی yield است.

تابعی که در آن از yield استفاده شود، به جای یک تابع معمولی، یک Generator Function ایجاد می‌کند.

def numbers():
    yield 1
    yield 2
    yield 3

حالا اگر تابع را اجرا کنیم، یک Generator دریافت می‌کنیم:

result = numbers()

print(result)

Generator هنوز همه مقادیر را تولید نکرده است؛ هر مقدار زمانی تولید می‌شود که آن را درخواست کنیم.

استفاده از next() با Generator

برای دریافت مقدار بعدی Generator می‌توانیم از تابع next() استفاده کنیم.

def numbers():
    yield 10
    yield 20
    yield 30

numbers_generator = numbers()

print(next(numbers_generator))
print(next(numbers_generator))
print(next(numbers_generator))

خروجی:

10
20
30

هر بار که next() را اجرا می‌کنیم، Generator از همان نقطه‌ای که متوقف شده بود ادامه پیدا می‌کند.

Generator چگونه وضعیت خود را حفظ می‌کند؟

یکی از ویژگی‌های مهم Generator این است که بعد از رسیدن به yield متوقف می‌شود و وضعیت اجرای تابع را حفظ می‌کند.

def test():
    print("شروع")
    yield 1

    print("ادامه")
    yield 2

generator = test()

print(next(generator))
print(next(generator))

در فراخوانی اول، تابع تا اولین yield اجرا می‌شود. در فراخوانی دوم، اجرای تابع از همان نقطه ادامه پیدا می‌کند.

استفاده از Generator با حلقه for

معمولاً برای پیمایش Generator نیازی نیست چندین بار next() را دستی صدا بزنیم. می‌توانیم از حلقه for استفاده کنیم.

def numbers():
    for i in range(1, 6):
        yield i

for number in numbers():
    print(number)

خروجی:

1
2
3
4
5

تفاوت Generator و List

یکی از سوالات مهم این است که چرا همیشه از List استفاده نکنیم؟

تفاوت اصلی در نحوه تولید و نگهداری داده‌ها در حافظه است.

ویژگی List Generator
تولید داده معمولاً یکجا مرحله‌ای
مصرف حافظه بیشتر برای داده‌های بزرگ معمولاً کمتر
دسترسی با index دارد ندارد
مناسب برای داده‌های حجیم گاهی محدودکننده بسیار مناسب

یک مثال ساده و کاربردی

فرض کنید می‌خواهیم اعداد زوج را یکی‌یکی تولید کنیم:

def even_numbers(limit):
    for number in range(2, limit + 1, 2):
        yield number

for number in even_numbers(10):
    print(number)

خروجی:

2
4
6
8
10

Generator Expression چیست؟

Generator Expression از نظر ساختار شبیه List Comprehension است، اما به جای ساخت یک لیست، یک Generator ایجاد می‌کند.

List Comprehension:

numbers = [x * 2 for x in range(5)]

print(numbers)

Generator Expression:

numbers = (x * 2 for x in range(5))

print(numbers)

در مثال دوم، مقادیر در ابتدا به صورت یک لیست کامل ساخته نمی‌شوند و هنگام نیاز تولید خواهند شد.

مثال کاربردی: خواندن فایل بزرگ

فرض کنید یک فایل متنی بسیار بزرگ داریم و می‌خواهیم خطوط آن را یکی‌یکی پردازش کنیم.

def read_lines(filename):
    with open(filename, "r", encoding="utf-8") as file:
        for line in file:
            yield line.strip()

for line in read_lines("data.txt"):
    print(line)

در این روش لازم نیست کل فایل را یکجا داخل حافظه قرار دهیم. خطوط فایل به صورت مرحله‌ای پردازش می‌شوند.

تفاوت return و yield

return مقدار تابع را برمی‌گرداند و اجرای تابع را تمام می‌کند، اما yield مقدار را تولید می‌کند و اجرای Generator را موقتاً متوقف می‌کند.

def normal_function():
    return 10

def generator_function():
    yield 10

تابع اول یک مقدار معمولی برمی‌گرداند، اما تابع دوم یک Generator ایجاد می‌کند.

StopIteration چیست؟

وقتی تمام مقادیر Generator مصرف شوند، دیگر مقداری برای تولید وجود ندارد. در این حالت فراخوانی مستقیم next() باعث ایجاد خطای StopIteration می‌شود.

def numbers():
    yield 1
    yield 2

generator = numbers()

print(next(generator))
print(next(generator))
print(next(generator))

فراخوانی سوم باعث پایان یافتن Generator می‌شود. البته هنگام استفاده از حلقه for این موضوع به صورت خودکار مدیریت می‌شود.

نکات مهم درباره Generator

  • Generator داده‌ها را معمولاً به صورت مرحله‌ای تولید می‌کند.
  • کلمه کلیدی اصلی برای ساخت Generator، yield است.
  • با next() می‌توان مقدار بعدی را دریافت کرد.
  • Generator برای پردازش داده‌های حجیم بسیار مفید است.
  • Generator معمولاً فقط یک بار قابل پیمایش است.
  • Generator مانند List دسترسی مستقیم با index ندارد.

تمرین‌های Generator در پایتون

تمرین ۱

یک Generator بنویسید که اعداد ۱ تا ۱۰ را یکی‌یکی تولید کند.

تمرین ۲

یک Generator بنویسید که اعداد زوج بین ۱ تا ۲۰ را تولید کند.

تمرین ۳

یک Generator بنویسید که مربع اعداد ۱ تا ۱۰ را تولید کند.

تمرین ۴

با استفاده از Generator Expression، مربع اعداد ۱ تا ۱۰ را ایجاد کنید.

سوالات متداول

Generator در پایتون چه کاربردی دارد؟

Generator برای تولید مرحله‌ای داده‌ها و مخصوصاً پردازش مجموعه‌های بزرگ داده کاربرد دارد و می‌تواند مصرف حافظه را کاهش دهد.

yield چه تفاوتی با return دارد؟

return اجرای تابع را تمام می‌کند، اما yield مقدار را تولید کرده و اجرای Generator را موقتاً متوقف می‌کند.

آیا Generator یک List است؟

خیر. Generator یک شیء قابل پیمایش است که مقادیر را معمولاً هنگام نیاز تولید می‌کند.

آیا Generator همیشه بهتر از List است؟

خیر. انتخاب بین آن‌ها به نوع مسئله بستگی دارد. Generator برای پردازش مرحله‌ای و داده‌های بزرگ بسیار مناسب است، در حالی که List برای دسترسی چندباره و استفاده از index کاربرد بیشتری دارد.