یه تفاوت خیلی کوچیک توی کد پایتون میتونه تعیین کنه برنامه‌تون چند مگابایت RAM مصرف کنه یا چند گیگابایت. ماجرا از جایی جالب میشه که دو تکه کد تقریباً شبیه به هم، خروجی یکسانی تولید میکنن ولی رفتار کاملاً متفاوتی با حافظه دارن؛ یکی همه داده‌ها رو یکجا میسازه و نگه میداره، اون یکی فقط وقتی لازم باشه داده بعدی رو تولید میکنه.

اینجاست که تفاوت بین List و Generator خودش رو نشون میده.

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

مشکل List دقیقاً چیه؟

List در پایتون یه ساختار داده واقعیه که اعضاش داخل حافظه نگهداری میشن. یعنی وقتی چنین چیزی میسازید، پایتون باید نتیجه همه محاسبات رو از همون ابتدا آماده کنه.

مثلاً:

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

for number in numbers:
    print(number)

اینجا قبل از اینکه حلقه for حتی شروع به کار کنه، پایتون باید میلیون‌ها مقدار رو محاسبه کنه و داخل numbers قرار بده. در نتیجه زمان شروع کار بیشتر میشه و مقدار قابل توجهی RAM هم مصرف میشه.

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

Generator چه فرقی داره؟

Generator قرار نیست تمام داده‌ها رو یکجا بسازه. به جای اون، هر مقدار زمانی تولید میشه که واقعاً بهش نیاز داشته باشید.

کافیه توی مثال قبلی کروشه [] رو با پرانتز () عوض کنیم:

numbers = (x * 2 for x in range(10_000_000))

for number in numbers:
    print(number)

ظاهر تغییر خیلی کوچیکه، ولی رفتار برنامه کاملاً فرق میکنه. این بار پایتون میلیون‌ها عدد رو داخل حافظه ذخیره نمیکنه؛ هر بار یه مقدار تولید میشه، استفاده میشه و بعد برنامه میره سراغ مقدار بعدی.

به این مدل اجرای مرحله‌ای معمولاً Lazy Evaluation گفته میشه.

چرا Generator برای فایل‌های بزرگ عالیه؟

فرض کنید یه فایل Log چند گیگابایتی دارید و میخواید فقط خط‌هایی که شامل ERROR هستن بررسی کنید. اگر اول کل فایل رو با read() داخل حافظه بریزید، ممکنه مصرف RAM به شدت بالا بره.

ولی اگه فایل رو خط‌به‌خط پردازش کنید، در هر لحظه فقط بخش کوچیکی از داده داخل حافظه قرار داره. این دقیقاً همون فلسفه‌ایه که Generator هم استفاده میکنه: چیزی رو قبل از اینکه لازم باشه نساز.

این مدل برای پردازش Log، CSVهای بزرگ، داده‌های دیتابیس، Web Scraping و Pipelineهای پردازش داده خیلی کاربردیه.

yield همون چیزی که Generator رو میسازه

Generator فقط با پرانتز ساخته نمیشه. میتونید با yield هم تابع Generator بنویسید.

فرق yield با return اینه که return اجرای تابع رو کامل تموم میکنه، ولی yield مقدار رو برمیگردونه و وضعیت تابع رو نگه میداره تا دفعه بعد از همون نقطه ادامه پیدا کنه.

یعنی تابع لازم نیست همه نتیجه‌ها رو همزمان تولید کنه.

این ویژگی مخصوصاً زمانی مفیده که تولید هر داده هزینه داشته باشه یا اصلاً ندونید چند مقدار قراره در نهایت تولید بشه.

Generator همیشه بهتره؟ نه!

اینجا هم مثل بیشتر بحث‌های Performance جواب مطلق نداریم.

اگر یه مجموعه داده کوچیک دارید و قراره چند بار روی اون پیمایش کنید، List معمولاً انتخاب راحت‌تر و حتی منطقی‌تریه. چون داده‌ها از قبل داخل حافظه هستن و میتونید هر چند بار که خواستید بهشون دسترسی داشته باشید.

Generator معمولاً یه بار مصرفه. وقتی کامل روش پیمایش کردید، دیگه از ابتدا قابل استفاده نیست مگر اینکه دوباره Generator جدید بسازید.

همچنین اگه نیاز دارید مستقیماً به عنصر شماره ۵۰۰ دسترسی داشته باشید، List مناسب‌تره؛ Generator دسترسی Random Access مثل items[500] در اختیارتون نمیذاره.

اشتباه رایج: تبدیل Generator دوباره به List

یکی از بامزه‌ترین اشتباه‌ها اینه که کسی برای کاهش مصرف حافظه Generator میسازه و بعد چند خط پایین‌تر این کار رو انجام میده:

list(generator)

با این کار دوباره تمام داده‌ها وارد حافظه میشن و تقریباً مزیت اصلی Generator رو از بین بردید.

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

از کجا بفهمیم باید Generator استفاده کنیم؟

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

مثلاً اگر قراره روی ۱۰ میلیون رکورد یکی‌یکی عملیات انجام بدید، ذخیره کردن همه اون‌ها داخل یه List معمولاً ضرورتی نداره. ولی اگر فقط ۵۰ محصول دارید که قراره چند بار Sort و Filter بشن، احتمالاً List انتخاب ساده‌تر و بهتره.

هدف Generator این نیست که List رو حذف کنه؛ هدفش اینه که وقتی لازم نیست همه داده توی RAM باشه، مجبور نباشید هزینه نگهداری همه‌ش رو پرداخت کنید.

جمع بندی

تفاوت List و Generator شاید در ظاهر فقط تفاوت بین [] و () باشه، ولی پشت این تغییر کوچیک یه تفاوت بزرگ در مدیریت حافظه وجود داره. List همه داده‌ها رو از ابتدا میسازه و داخل RAM نگه میداره، در حالی که Generator مقدارها رو فقط زمانی تولید میکنه که واقعاً بهشون نیاز داشته باشید.

برای داده‌های کوچک، List کاملاً مناسب و راحت‌تره؛ اما وقتی پای میلیون‌ها رکورد، فایل‌های بزرگ یا پردازش‌های طولانی وسط باشه، Generator میتونه مصرف حافظه رو به شکل چشمگیری کاهش بده.

پس دفعه بعد که خواستید یه List چند میلیونی بسازید، قبلش یه سؤال ساده از خودتون بپرسید: واقعاً لازم دارم همه این داده‌ها همین الان داخل RAM باشن؟

اگه جواب «نه» بود، احتمالاً Generator انتخاب بهتریه.