جنراتور ها (generator) کاملا شبیه توابع عادی در پایتون هستند با این تفاوت که generator ها از yield به جای return استفاده می کنند. تفاوت yield و return در این است که در توابع عادی وقتی interpreter به return می رسد جواب را برمی گرداند و از برنامه خارج می شود ولی در yield جواب برگشت داده می شود ولی از تابع خارج نمی شویم و برنامه به کار خود ادامه می دهد. کار اصلی yield کنترل جریان تابع generator است. زمانی که از یک generator یا generator expression استفاده می کنیم یک iterator که generator نامیده می شود را بر می گرداند. زمانی که interpreter در برنامه به yield بر می خورد، برنامه به حال تعلیق در می آید، state آن ذخیره شده و مقدار تابع yield می شود برعکس return که جریان تابع به طور کامل متوقف شده و مقدار return می شود.
در مواقعی که قرار است با یک مجموعه داده بزرگ کار کنیم، یا یک پردازش پیچیده داشته باشیم که هر بار که فراخوانی می شود که باید state خود را نگه دارد و موارد دیگر، generator ها کارکرد فوق العاده خود را نشان می دهند. در حقیقت می توان گفت generator ها می توانند state قبلی خود را حفظ کنند.
جنراتور ها توابع خاصی در پایتون هستند که یک lazy iterator را برمی گردانند. lazy iterator که به آن call-by-need نیز گفته می شود یک استراتژی ارزیابی است که ارزیابی یک عبارت را تا زمان نیاز به مقدار آن به تأخیر می اندازد (ارزیابی غیر سختگیرانه) و همچنین از ارزیابی های مکرر (به اشتراک گذاری) جلوگیری می کند. به اشتراک گذاری می تواند باعث کاهش زمان اجرای برخی از توابع توسط یک عامل نمایشی نسبت به سایر استراتژی های ارزیابی غیر سختگیرانه شود. lazy iterator مانند لیست ها قابلیت iterate شدن را دارند ولی بر خلاف لیست ها قابلیت ذخیره در memory را ندارند.
def csv_reader(file_name): file = open(file_name) result = file.read().split("\n") return result
در مثال فوق تابع open همه محتویات فایل را به یکباره لود میکند و یک iterator در اختیار ما قرار می دهد که می توان روی آن iterate کرد. حال اگر فایل ارسال شده به تابع csv_reader بزرگتر از سایز مموری آزاد باشد چه اتفاقی می افتد؟
بله MemoryError
حال مثال بالا را به صورت زیر پیاده سازی می کنیم که
def csv_reader(file_name): for row in open(file_name, "r"): yield row
در مثال بالا روی هر سطر iterate می کنیم و به جای return کردن نتیجه آن را yield میکنیم.
مثال بالا را با generator comprehension هم می توان پیاده سازی کرد. generator comprehension شبیه list expression است با این تفاوت که به جای [ ] از ( ) استفاده می کنیم. با استفاده از generator comprehension می توان سریع تر و راحت تر generator تعریف کرد.برتری دیگر این روش این است که زمانی که از generator comprehension استفاده می کنیم سربار کمتری روی حافظه خواهیم داشت.
کد زیر و مثال بالا معادل همدیگر هستند:
csv_gen = (row for row in open(file_name))
همانگونه که قبلا اشاره کردیم generator ها در بهینه سازی memory فوق العاده عمل می کنند. مثال زیر مشخص می کند حجم آبجکت generator در مقابل list حدودا 700 برابر کوچکتر است.
>>> import sys >>> nums_squared_lc = [i * 2 for i in range(10000)] >>> sys.getsizeof(nums_squared_lc) 87624 >>> nums_squared_gc = (i ** 2 for i in range(10000)) >>> print(sys.getsizeof(nums_squared_gc)) 120
نکته: در مواردی که حجم لیست از حجم memory خیلی کوچکتر باشد list comprehension در مقایسه با generator comprehension سریع تر عمل می کند.
def infinite_sequence(): num = 0 while True: yield num num += 1 for i in infinite_sequence(): print(i, end=" ")
خروجی کد بالا به صورت زیر خواهد بود و تا زمانی keyboard interrupt که رخ ندهد ادامه خواهد داشت:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 [...] 6157818 6157819 6157820 6157821 6157822 6157823 6157824 6157825 6157826 6157827 6157828 6157829 6157830 6157831 6157832 6157833 6157834 6157835 6157836 6157837 6157838 6157839 6157840 6157841 6157842 KeyboardInterrupt Traceback (most recent call last): File "<stdin>", line 2, in <module>
برنامه فوق را با استفاده از ()next پیاده سازی کرد. از آنجایی که تابع next روی iterator ها قابل اجراست می توان به این مساله پی برد که خروجی generator ها iterator است.
>>> gen = infinite_sequence() >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) 2 >>> next(gen) 3