تا حالا برات پیش اومده بخوای یه لیست خیلی بزرگ (مثلاً با چند میلیون آیتم) رو تو پایتون پردازش کنی و یهو ببینی برنامه کُند شده یا کل رَم (RAM) سیستمت پر شده؟ اینجاست که دو تا قهرمان به نام‌های ایتراتور (Iterator) و ژنراتور (Generator) وارد بازی میشن. 

ایتراتور (Iterator) اصلاً یعنی چی؟

فرض کن یه جعبه پر از توپ‌ های رنگی داری. ایتراتور مثل آدمی میمونه که دستش رو میکنه تو جعبه و یکی ‌یکی توپ‌ ها رو بهت میده. تا وقتی تو ازش نگیری، توپ بعدی رو درنمیاره. تو پایتون، ایتراتور شیئ هست که میتونه روی یه مجموعه از داده ‌ها حرکت کنه (Iterate بشه). دو تا ویژگی اصلی داره:

  1. متد __iter__() رو داره که خود شیء رو برمیگردونه.
  2. متد __next__() رو داره که آیتم بعدی رو میده. وقتی آیتم‌ ها تموم بشن، یه خطای خاص به اسم StopIteration میده تا پایتون بفهمه کار تموم شده.

یه مثال ساده از ایتراتور:

وقتی از حلقه‌ی for استفاده میکنی، پایتون پشت صحنه داره از همین ایتراتورها استفاده میکنه!

my_list = [10, 20, 30]
my_iterator = iter(my_list)
print(next(my_iterator)) # خروجی: 10
print(next(my_iterator)) # خروجی: 20
print(next(my_iterator)) # خروجی: 30
# print(next(my_iterator)) # اگه اینو از کامنت دربیاری، خطای StopIteration میده!

ژنراتور (Generator): برادر کوچیک ‌تر و زرنگ ‌تر!

ساختن یه ایتراتور از صفر (با نوشتن کلاس و متدهای __iter__ و __next__) یه کم طولانی و خسته‌کننده‌ ست. پایتون اومده کار رو راحت کرده و ژنراتورها رو معرفی کرده. ژنراتور در واقع یه نوع خاص از ایتراتوره که خیلی راحت‌ تر ساخته میشه. کافیه یه تابع معمولی بنویسی، ولی به جای اینکه از کلمه return استفاده کنی، از کلمه جادویی yield استفاده کنی!

فرق yield و return تو چیه؟

  • وقتی تابع به return میرسه، کارش تموم میشه و همه چی رو فراموش میکنه.
  • وقتی تابع به yield میرسه، مقدار رو برمی‌گردونه اما متوقف (Pause) میشه! یعنی یادش میمونه دفعه قبل تا کجا پیش رفته بود و دفعه بعد که صداش بزنی، از همونجا ادامه میده.

یه مثال از ژنراتور:

بیا یه شمارشگر معکوس بسازیم:

def countdown(num):
    print("شمارشگر شروع شد!")
    while num > 0:
        yield num
        num -= 1
# حالا چطور ازش استفاده کنیم؟
counter = countdown(3)
print(next(counter)) # چاپ میکنه: شمارشگر شروع شد! بعد میده: 3
print(next(counter)) # میده: 2
print(next(counter)) # میده: 1

مقایسه سریع: Iterator در برابر Generator

برای اینکه تفاوتشون کامل تو ذهنت جا بیفته، این جدول رو ببین:

ویژگی ایتراتور (Iterator) ژنراتور (Generator)
روش ساخت با استفاده از کلاس‌ها و متدهای __iter__ و __next__ با استفاده از توابع و کلمه کلیدی yield
میزان کدنویسی طولانی و نیازمند کد های Boilerplate (تکراری) بسیار کوتاه، تمیز و خوانا
وضعیت داخلی خودت باید متغیر ها رو مدیریت کنی پایتون خودش وضعیت متغیر ها رو حفظ میکنه
مصرف حافظه بسیار بهینه (چون داده‌ها رو یکی ‌یکی میاره) بسیار بهینه (مانند ایتراتورها)

چرا اینقدر مهمن؟ (جادوی مدیریت حافظه)

مهم ترین دلیل استفاده از ژنراتور ها صرفه‌ جویی در حافظه (Memory) است. فرض کن میخوایم مربع اعداد ۱ تا ۱ میلیون رو حساب کنیم. اگه این کار رو با لیست معمولی انجام بدیم، پایتون هر ۱ میلیون عدد رو تو حافظه رَم ذخیره می‌کنه! اما با ژنراتور، در هر لحظه فقط یک عدد تو حافظه است. ببین چقدر فرق دارن

import sys
# روش اول: ساخت لیست (List Comprehension)
my_list = [x**2 for x in range(1000000)]
print(f"حجم لیست: {sys.getsizeof(my_list)} بایت") 
# خروجی یه عدد خیلی بزرگه (حدود 8 مگابایت)
# روش دوم: ساخت ژنراتور (Generator Expression)
my_gen = (x**2 for x in range(1000000))
print(f"حجم ژنراتور: {sys.getsizeof(my_gen)} بایت") 
# خروجی فقط حدود 100 بایته! فوق‌العاده نیست؟

نکته: برای ساخت ژنراتور تو یه خط، دقیقاً مثل لیست عمل میکنیم با این تفاوت که به جای براکت [] از پرانتز () استفاده میکنیم.

جمع‌بندی

اگه با داده‌ های حجیم سروکار داری (مثل خوندن فایل‌ های متنی چند گیگابایتی، پردازش لاگ‌ های سرور، یا استریم داده‌ ها)، حتماً از Generator ها استفاده کن. هم کدت تمیز تر میشه، هم سیستمت ازت تشکر میکنه! امیدوارم این مقاله برات شفاف و کاربردی بوده باشه.