ویدیو آموزش generator در پایتون
پیش نیازها:
آیا تا به حال مجبور شده اید با مجموعه داده ای آنقدر بزرگ کار کنید که حافظه دستگاه شما را تحت تأثیر قرار دهد؟ یا شاید شما یک تابع پیچیده دارید که هر بار که فراخوانی می شود نیاز به حفظ یک حالت داخلی دارد، اما این تابع برای توجیه ایجاد کلاس خود، بسیار کوچک است. در این موارد و موارد دیگر، ژنراتورها و عبارت yield پایتون اینجا هستند تا به شما کمک کنند.
# استفاده از generatorهای پایتون
توابع ژنراتور که با PEP 255 معرفی شدند، نوع خاصی از تابع هستند که یک تکرار کننده تنبل(lazy iterator) را برمی گرداند. اینها اشیایی هستند که می توانید مانند یک لیست روی آنها حلقه بزنید. با این حال، بر خلاف لیست ها، تکرار کننده های تنبل محتویات خود را در حافظه ذخیره نمی کنند.
اکنون که ایده ای تقریبی از کاری که یک ژنراتور انجام می دهد دارید، ممکن است تعجب کنید که آنها در عمل چگونه به نظر می رسند. بیایید به دو مثال نگاهی بیندازیم. در قسمت اول، نحوه عملکرد ژنراتورها را به طور کلی بررسی خواهیم کرد. سپس، بزرگنمایی می کنید و هر مثال را با دقت بیشتری بررسی می کنید.
+ مثال اول: خواندن فایلهای بزرگ
یک مورد معمول استفاده از ژنراتورها کار با جریان داده یا فایل های بزرگ مانند فایل های CSV است. این فایل های متنی داده ها را با استفاده از کاما به ستون ها جدا می کنند. این فرمت یک روش رایج برای اشتراک گذاری داده ها است. حال، اگر بخواهید تعداد ردیف های یک فایل CSV را بشمارید، چه؟ بلوک کد زیر یک راه برای شمارش آن ردیف ها را نشان می دهد:
csv_gen = csv_reader("some_csv.txt")
row_count = 0
for row in csv_gen:
row_count += 1
print(f"Row count is {row_count}")
با نگاهی به این مثال، ممکن است انتظار داشته باشید که csv_gen یک لیست باشد. برای پر کردن این لیست، ()csv_reader یک فایل را باز می کند و محتویات آن را در csv_gen بارگذاری می کند. سپس، برنامه روی لیست تکرار می شود و row_count را برای هر ردیف افزایش می دهد.
این یک توضیح منطقی است، اما اگر فایل بسیار بزرگ باشد، آیا این طرح همچنان کار می کند؟ اگر فایل از حافظه ای که در دسترس دارید بزرگتر باشد چه؟ برای پاسخ به این سوال، فرض کنید ()csv_reader فقط فایل را باز می کند و آن را در یک آرایه می خواند:
def csv_reader(file_name):
file = open(file_name)
result = file.read().split("\n")
return result
این تابع یک فایل مشخص را باز می کند و از file.read() همراه با .split() برای اضافه کردن هر خط به عنوان یک عنصر جداگانه به یک لیست استفاده می کند. اگر بخواهید از این نسخه csv_reader() در بلوک کد شمارش ردیفی که در بالا مشاهده کردید استفاده کنید، خروجی زیر را دریافت خواهید کرد:
Traceback (most recent call last):
File "ex1_naive.py", line 22, in <module>
main()
File "ex1_naive.py", line 13, in main
csv_gen = csv_reader("file.txt")
File "ex1_naive.py", line 6, in csv_reader
result = file.read().split("\n")
MemoryError
در این حالت، open() یک شی generator را برمیگرداند که میتوانید با تنبلی آن را خط به خط تکرار کنید. با این حال، file.read().split() همه چیز را به یکباره در حافظه بارگذاری می کند و باعث ایجاد خطای حافظه می شود.
قبل از اینکه این اتفاق بیفتد، احتمالاً متوجه خواهید شد که رایانه شما دچار کندی سرعت شده است. حتی ممکن است لازم باشد برنامه را با وقفه Keyboard متوقف کنید. بنابراین، چگونه می توانید این فایل های داده عظیم را مدیریت کنید؟ نگاهی به تعریف جدیدی از csv_reader():
def csv_reader(file_name):
for row in open(file_name, "r"):
yield row
در این نسخه، فایل را باز میکنید، آن را تکرار میکنید و یک ردیف ایجاد میکنید. این کد باید خروجی زیر را بدون خطای حافظه تولید کند:
Row count is 64186394
اینجا چه اتفاقی دارد میافتد؟ خب، شما اساساً ()csv_reader را به یک تابع generator تبدیل کرده اید. این نسخه یک فایل را باز می کند، در هر خط حلقه می زند و به جای بازگرداندن هر سطر، آن را yield می کند.
شما همچنین می توانید یک عبارت generator (که به آن generator comprehension نیز گفته می شود) تعریف کنید که دارای نحو بسیار مشابه list comprehension است. به این ترتیب، می توانید از ژنراتور بدون فراخوانی تابع استفاده کنید:
csv_gen = (row for row in open(file_name))
این یک راه مختصرتر برای ایجاد لیست csv_gen است. به زودی در مورد دستور yield پایتون بیشتر خواهید آموخت. در حال حاضر، فقط این تفاوت کلیدی را به خاطر بسپارید:
- استفاده از yield منجر به یک شی generator می شود
- استفاده از return تنها در خط اول فایل کار میکند
+ مثال دوم: ساخت دنباله بینهایت
بیایید دنده ها را عوض کنیم و به تولید دنباله بی نهایت نگاه کنیم. در پایتون، برای به دست آوردن یک دنباله محدود، range() را فراخوانی کرده و آن را در یک list ارزیابی می کنید:
>>> a = range(5)
>>> list(a)
[0, 1, 2, 3, 4]
با این حال، تولید یک دنباله بی نهایت نیاز به استفاده از یک ژنراتور دارد، زیرا حافظه رایانه شما محدود است:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
این بلوک کد کوتاه و شیرین است. ابتدا متغیر num را مقداردهی اولیه کرده و یک حلقه بی نهایت راه اندازی می کنید. سپس، بلافاصله num را yield میکنید تا بتوانید حالت اولیه را بگیرید. این عمل range() را تقلید می کند.
پس از yield، عدد را با 1 افزایش می دهید. اگر این را با حلقه for امتحان کنید، خواهید دید که واقعا بی نهایت به نظر می رسد:
>>> for i in infinite_sequence():
... print(i, end=" ")
...
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>
این برنامه تا زمانی که آن را به صورت دستی متوقف نکنید به اجرای آن ادامه خواهد داد.
به جای استفاده از حلقه for، میتوانید مستقیماً () next را روی generator پایتون فراخوانی کنید. این به ویژه برای آزمایش یک ژنراتور در کنسول مفید است:
>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3
در اینجا، شما یک generator به نام gen دارید که به صورت دستی با فراخوانی مکرر next () آن را تکرار می کنید. این به عنوان یک بررسی سلامت عقل عالی عمل می کند تا مطمئن شوید که ژنراتورهای شما خروجی مورد انتظار شما را تولید می کنند.
+ مثال سوم: تشخیص پالیندروم
شما می توانید از دنبالههای بی نهایت به طرق مختلف استفاده کنید، اما یکی از کاربردهای عملی آنها در ساخت آشکارسازهای پالیندروم است. یک آشکارساز پالیندروم تمام دنبالههای حروف یا اعداد را که پالیندروم هستند پیدا میکند. اینها کلمات یا اعدادی هستند که به صورت یکسان به جلو و عقب خوانده می شوند، مانند 121. ابتدا، آشکارساز پالیندروم عددی خود را تعریف کنید:
def is_palindrome(num):
# Skip single-digit inputs
if num // 10 == 0:
return False
temp = num
reversed_num = 0
while temp != 0:
reversed_num = (reversed_num * 10) + (temp % 10)
temp = temp // 10
if num == reversed_num:
return num
else:
return False
در مورد درک ریاضیات اساسی در این کد زیاد نگران نباشید. فقط توجه داشته باشید که تابع یک عدد ورودی می گیرد، آن را برعکس می کند و بررسی می کند که آیا عدد معکوس شده با عدد اصلی یکسان است یا خیر. اکنون می توانید از ژنراتور توالی بی نهایت خود برای دریافت لیست در حال اجرا از تمام پالیندروم های عددی استفاده کنید:
>>> for i in infinite_sequence():
... pal = is_palindrome(i)
... if pal:
... print(i)
...
11
22
33
[...]
99799
99899
99999
100001
101101
102201
KeyboardInterrupt
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "<stdin>", line 5, in is_palindrome
در این حالت، تنها اعدادی که روی کنسول چاپ می شوند، همان اعدادی هستند که به جلو یا عقب هستند.
اکنون که یک مورد استفاده ساده برای یک generator توالی نامتناهی دیدید، بیایید عمیقتر به نحوه عملکرد ژنراتورها بپردازیم.
# درک generatorهای پایتون
تا اینجا، شما در مورد دو روش اصلی ایجاد generatorها یاد گرفته اید: با استفاده از توابع ژنراتور و عبارات ژنراتور. حتی ممکن است درک شهودی از نحوه عملکرد ژنراتورها داشته باشید. بیایید یک لحظه وقت بگذاریم تا آن دانش را کمی واضح تر کنیم.
توابع ژنراتور درست مانند توابع معمولی به نظر می رسند و عمل می کنند، اما با یک مشخصه تعیین کننده. توابع ژنراتور به جای return از کلمه کلیدی yield پایتون استفاده می کنند. تابع generator را که قبلا نوشتید به یاد بیاورید:
def infinite_sequence():
num = 0
while True:
yield num
num += 1
این به نظر یک تعریف تابع معمولی است، به جز عبارت yield پایتون و کدی که به دنبال آن است. yield نشان می دهد که یک مقدار به تماس گیرنده ارسال می شود، اما بر خلاف return، بعد از آن از تابع خارج نمی شوید.
در عوض، وضعیت(state) تابع به خاطر سپرده می شود. به این ترتیب، زمانی که next() بر روی یک generator فراخوانی می شود (به طور صریح یا ضمنی در یک حلقه for)، متغیر شماره قبلی افزایش یافته و سپس دوباره به دست می آید. از آنجایی که توابع generator شبیه توابع دیگر هستند و بسیار شبیه به آنها عمل می کنند، می توانید فرض کنید که عبارات generator بسیار شبیه سایر comprehensionهای موجود در پایتون هستند.
+ ساخت عبارات ژنراتور پایتون(Generator Expressions)
مانند list comprehensionها، عبارات genereator به شما اجازه می دهد تا به سرعت یک شی genereator را تنها در چند خط کد ایجاد کنید. آنها همچنین در موارد مشابهی که از list comprehensionها استفاده می شود، با یک مزیت اضافی مفید هستند: می توانید آنها را بدون ساختن و نگه داشتن کل شی در حافظه قبل از تکرار ایجاد کنید. به عبارت دیگر، هنگام استفاده از عبارات genereator، هیچ جریمه حافظه ای نخواهید داشت. این مثال از مربع کردن برخی اعداد را در نظر بگیرید:
>>> nums_squared_lc = [num**2 for num in range(5)]
>>> nums_squared_gc = (num**2 for num in range(5))
هر دو nums_squared_lc و nums_squared_gc اساساً یکسان به نظر می رسند، اما یک تفاوت اساسی وجود دارد. آیا می توانید آن را تشخیص دهید؟ وقتی هر یک از این اشیاء را بررسی می کنید، به اتفاقاتی که می افتد نگاه کنید:
>>> nums_squared_lc
[0, 1, 4, 9, 16]
>>> nums_squared_gc
<generator object <genexpr> at 0x107fbbc78>
شی اول از براکت برای ساختن یک لیست استفاده کرد، در حالی که شی دوم با استفاده از پرانتز یک عبارت generator ایجاد کرد. خروجی تأیید می کند که یک شی generator ایجاد کرده اید و از یک لیست متمایز است.
+ مشخص کردن پرفورمنس ژنراتور پایتون
قبلاً یاد گرفتید که ژنراتورها یک راه عالی برای بهینه سازی حافظه هستند. در حالی که یک ژنراتور دنباله نامتناهی یک مثال افراطی از این بهینهسازی است، بیایید نمونههای مربعسازی اعدادی را که بهتازگی دیدهاید تقویت کنیم و اندازه اشیاء حاصل را بررسی کنیم. می توانید این کار را با یک فراخوانی به sys.getsizeof() انجام دهید:
>>> 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
در این مورد، لیستی که از list comprehension دریافت می کنید 87624 بایت است، در حالی که شی generator تنها 120 بایت است. این به این معنی است که لیست بیش از 700 برابر بزرگتر از شی generator است!
با این حال یک چیز را باید در نظر داشت. اگر لیست کوچکتر از حافظه در دسترس ماشین در حال اجرا باشد، در این صورت list comprehension می تواند سریعتر از عبارت generator معادل ارزیابی شود. برای کشف این موضوع، اجازه دهید نتایج حاصل از دو comprehension بالا را جمع بندی کنیم. میتوانید با cProfile.run() اینکار را انجام دهید:
>>> import cProfile
>>> cProfile.run('sum([i * 2 for i in range(10000)])')
5 function calls in 0.001 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.001 0.001 <string>:1(<listcomp>)
1 0.000 0.000 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.sum}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
>>> cProfile.run('sum((i * 2 for i in range(10000)))')
10005 function calls in 0.003 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
10001 0.002 0.000 0.002 0.000 <string>:1(<genexpr>)
1 0.000 0.000 0.003 0.003 <string>:1(<module>)
1 0.000 0.000 0.003 0.003 {built-in method builtins.exec}
1 0.001 0.001 0.003 0.003 {built-in method builtins.sum}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
در اینجا، میتوانید ببینید که جمع کردن همه مقادیر در list comprehension، تقریباً یک سوم زمان جمعآوری در generator را به خود اختصاص داده است. اگر سرعت مشکل است و حافظه مشکلی ندارد، احتمالاً list comprehension ابزار بهتری برای کار است.
به یاد داشته باشید، list comprehensions، لیست های کامل را برمی گرداند، در حالی که عبارات generator، ژنراتورها را برمی گرداند. ژنراتورها چه از یک تابع یا یک عبارت ساخته شده باشند یکسان کار می کنند. استفاده از یک عبارت فقط به شما امکان می دهد generatorهای ساده را در یک خط مشخص کنید، با بازدهی فرضی در پایان هر تکرار داخلی.
دستور yield پایتون مطمئناً پایهای است که تمام عملکرد ژنراتورها بر آن استوار است، بنابراین بیایید به نحوه عملکرد yield در پایتون بپردازیم.
# درک دستور yield پایتون
در کل، yield یک دستور نسبتاً ساده است. وظیفه اصلی آن کنترل جریان یک تابع generator به روشی شبیه به دستورات بازگشتی است. همانطور که در بالا به طور خلاصه ذکر شد، دستور yield پایتون چند ترفند در آستین خود دارد.
هنگامی که یک تابع generator را فراخوانی می کنید یا از یک عبارت generator استفاده می کنید، یک تکرار کننده خاص به نام generator را برمی گردانید. شما می توانید این generator را به یک متغیر اختصاص دهید تا از آن استفاده کنید. وقتی متدهای خاصی را روی ژنراتور فراخوانی می کنید، مانند next()، کد داخل تابع تا yield اجرا می شود.
وقتی دستور yield پایتون زده میشود، برنامه اجرای تابع را به حالت تعلیق در میآورد و مقدار بازده را به تماسگیرنده برمیگرداند. (در مقابل، return اجرای تابع را به طور کامل متوقف می کند.) هنگامی که یک تابع به حالت تعلیق در می آید، وضعیت آن تابع ذخیره می شود. این شامل هر گونه اتصال متغیر محلی به generator، اشاره گر دستورالعمل، پشته داخلی، و هرگونه رسیدگی به استثنا می شود.
این به شما امکان می دهد هر زمان که یکی از متدهای generator را فراخوانی کردید، اجرای تابع را از سر بگیرید. به این ترتیب، تمام ارزیابی عملکرد بلافاصله پس از yield، از سر گرفته می شود. با استفاده از چند عبارت yield پایتون می توانید این را در عمل مشاهده کنید:
>>> def multi_yield():
... yield_str = "This will print the first string"
... yield yield_str
... yield_str = "This will print the second string"
... yield yield_str
...
>>> multi_obj = multi_yield()
>>> print(next(multi_obj))
This will print the first string
>>> print(next(multi_obj))
This will print the second string
>>> print(next(multi_obj))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
نگاهی دقیق تر به آخرین فراخوانی next() بیندازید. می توانید ببینید که اجرا با یک traceback متوقف شده است. این به این دلیل است که ژنراتورها، مانند همه تکرارکننده ها(iterators)، می توانند خسته شوند. مگر اینکه generator شما بی نهایت باشد، می توانید فقط یک بار از طریق آن تکرار کنید. هنگامی که همه مقادیر ارزیابی شدند، تکرار متوقف می شود و حلقه for خارج می شود. اگر از next() استفاده کردید، در عوض یک استثنا صریح StopIteration دریافت خواهید کرد.
yield را می توان به روش های بسیاری برای کنترل جریان اجرای ژنراتور استفاده کرد. تا آنجایی که خلاقیت شما اجازه می دهد، می توان از چندین عبارت yield پایتون استفاده کرد.
اگر ویدیو بالا را دوست داشتید، پیشنهاد میکنیم به مطالب زیر هم سر بزنید:
دوره های آموزش پروژه محور و پیشرفته پایتون
آشنایی با namedTuples در پایتون
ارسال نظر