علی عابدی
علی عابدی
خواندن ۱۰ دقیقه·۵ سال پیش

مفاهیمی در پایتون که شاید نمیدانید - بخش دوم - Generators

مقدمه

به دومین بخش از مجموعه‌ی مفاهیمی در پایتون که شاید نمیدانید خوش آمدید. اگر بخش اول را هنوز مطالعه نکرده‌اید میتوانید از طریق لینک زیر مقاله قبلی را مطالعه کنید. بخش اول پیش نیاز این بخش است.
مفاهیمی در پایتون که شاید نمیدانید - بخش اول - Iterators

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


بخش دوم - Generators

مقدمه - Generator چیست؟

در مقاله بخش اول با مفهوم Iterator در پایتون آشنا شدیم. واسه اینکه بتونیم مفهوم generator را توضیح بدیم، یه برگشت به مقاله قبلی میزنیم. آخر مقاله قبلی نوشتم که "شما خودتون هم میتونید iteratorهایی ایجاد کنید. کلاسی ایجاد کنید که متدهای __()iter__ و __()next__ را دارند و به همین راحتی iterator خودتون را ایجاد کنید." بیا یکم بیشتر در این باره حرف بزنیم.

همونطور که گفتیم برای ایجاد یک Iterator باید یک کلاس ایجاد کنیم و دو تا متد __()iter__ و __()next__ را داخلش پیاده سازی کنیم. به این مثال دقت کن:

نمونه‌ای از پیاده سازی دستی یک Iterator
نمونه‌ای از پیاده سازی دستی یک Iterator

در کد بسیار ساده بالا یک کلاس به نام Test ایجاد کردیم و دو متد __()iter__ و __()next__ را در اون پیاده سازی کردیم. در حقیقت کاری که این Iterator برای ما انجام میده اینه که هر بار یک عدد به قبلی اضافه میکنه و تا بی نهایت میتونیم ازش عدد بگیریم. خروجی کد بالا میشه:

1 2 3 4

اما این کار سخته؟ مگه نه؟ اگه قرار باشه برای هر Iterator که بخوایم ایجاد کنیم یه همچین کاری کنیم، عملا کسی سراغ استفاده از این قابلیت به صورت دستی نمیره. اینجاست که مفهوم Generator وارد بازی میشه!

اگه بخوایم Generator را تعریف کنیم، می‌تونیم بگیم یک فانکشن هست که یک Iterator Object را برای ما برمیگردونه. در نتیجه با استفاده از اون میتونیم یک شی بگیریم که میشه روش تکرار (Iteration) انجام داد.

حالا سوالی که پیش میاد اینه که چطوری می‌تونیم Generator ایجاد کنیم؟ به طور کلی، دور روش برای ایجاد Generator وجود داره.


چگونه Generator ایجاد کنیم؟

روش اول استفاده از yield توی فانکشن به جای return هست.
نکته خیلی مهم تفاوت yield و return هست. وقتی ما return انجام می‌دیم، دستورات بعد از return دیگه اجرا نمیشن چون که کنترل برنامه به طور کامل به جایی میره که فانکشن‌ ما صدا زده شده و اگر دوباره همون فانکشن صدا زده بشه، دوباره از اول اجرا میشه. توی دستور yield وقتی که فانکشن‌ ما میرسه به این دستور، کنترل برنامه به دست جایی که صدا زده شده داده میشه، تا اینجا شبیه به return هست، اما اگر دوباره برنامه ما همون فانکشن‌ را صدا بزنه (البته با شرایطی که جلوتر توضیح میدم)، از ادامه جایی که آخرین yield بوده فانکشن‌ شروع به اجرا شدن می‌کنه و متغیرهایی که توی اون فانکشن تغییر دادیم آخرین مقدارشون (نه مقدار اولیه) را دارن. یک مقدار این مفهوم گیج کنندست اول کار اما جلوتر مثال‌هایی میبینیم که واضح‌تر میشه برامون. یک نکته دیگه باید بگم که وقتی از yield توی یک فانکشن‌ استفاده کنیم، وقتی اون فانکشن‌ را صدا میزنیم، یک Generator Object برای ما ایجاد میشه برخلاف وقتی که از return استفاده می‌کنیم و اون فانکشن‌ اجرا میشه و return یک مقداری را برمیگردونه و ما همون مقداری که برگردونده را داریم. پس نمیتونیم با صدا زدن معمولی یک تابع که yield داره از اون اطلاعاتمون را دریافت کنیم چون خروجی اون تابع یک Object هست. در ادامه در این باره بیشتر حرف می‌زنیم.

وقتی ما از yield توی فانکشن‌ استفاده کنیم، اون فانکشن‌ میشه یک Generator Function که یک Generator Object برای ما برمیگردونه (منبع). بریم یک مثال ببینیم تا بیشتر جا بیوفته برامون.

مثال از generator function و generator object
مثال از generator function و generator object

توی مثال بالا می‌بینید که ما اومدیم یک فانکشن‌ به اسم generator_function ایجاد کردیم و توی اون یک حلقه for گذاشتیم که اعداد ۱ تا ۴ را برای ما با استفاده از دستور yield برمیگردونه. بعد از اون اومدیم یک حلقه for خارج از فانکشن‌ ایجاد کردیم و روی فانکشن‌ تکرار می‌کنیم و خروجی‌هاش را چاپ میکنیم. از مقاله قبلی دیگه میدونیم که داخل حلقه for چه اتفاقی میوفته. بخوایم بیشتر توضیح بدیم در حقیقت این قطعه کد میاد و روی generator object که فانکشن‌ ما برمیگردونه تکرار انجام میده تا جایی که StopIteration را raise کنه یعنی ساده تر بگیم تا جایی که دیگه yield نباشه توی فانکشن.

نکته هیجان انگیز و مهم اینه که بر خلاف متد معمولی، generator در هر بار اجرا یا تکرار یکی از خروجی‌هاش را ایجاد میکنه. منظور از "در هر بار اجرا" یعنی هربار که next روی اون generator object صدا زده بشه (میدونیم توی for هم این اتفاق میوفته در حقیقت). خوبیش چیه؟ بسیار بسیار Memory Efficient تره یعنی مثلا فرض کنید توی همین مثال ساده بالا ما اگه می‌خواستیم از متد معمولی استفاده کنیم، باید یک list شامل همه عددهامون برمیگردوندیم و برنامه باید برای یک لیست کامل مموری درخواست می‌کرد. اما وقتی از generator استفاده می‌کنیم در هر بار تکرار یکی از عددهامون ایجاد میشه نه همش با هم! (منبع)

در ادامه مثال قبل، اومدیم همون فانکشن‌ را صدا زدیم و داخل یک متغیر به اسم generator_object ریختیم. اگر type متغیرمون را چک کنیم میبینیم generator هست. اگر متد معمولی بود و return داشت، type متغیر ما برابر با type مقداری بود که فانکشن‌ برای ما برمی‌گردوند!

خروجی کد بالا میشه:

1 2 3 4 <class 'generator'> 1 2 3 4

توجه کنید که فانکشن داخل مثال بالا با فانکشن داخل مثال زیر یکی هستند و نباید فکر کنیم چون در فانکشن مثال بالا یک yield نوشته شده در واقع هم یک بار میتوان از ()next استفاده کرد، بلکه به تعداد تکرار حلقه for امکان صدا زدن ()next روی generator object ما وجود دارد.

خروجی کد بالا هم عینا مثل مثال قبل خواهد بود.

پس تا الان فهمیدیم که با استفاده از کلمه کلیدی yield چطور میتونیم generator ایجاد کنیم. اما استفاده از yield تنها راه ایجاد generator نیست.


روش دوم استفاده از generator expression.
احتمالا یک مقدار اگر با پایتون راحت باشید میدونید که با دستوری مثل دستور زیر، میشه یک list ایجاد کرد.

خروجی دستور بالا یک لیست است.

[1, 2, 3, 4]

ایجاد یک generator با استفاده از generator expression خیلی شبیه به دستور بالاست اما تفاوتش اینه که به جای براکت دور دستور باید از پرانتز استفاده کنیم به صورت زیر:

که خروجی دستور بالا به شکل زیر است:

<generator object <genexpr> at 0x7fc260d08308> 1 2 3 4

استفاده از generator expression ساده است و برای ایجاد generator به صورت on the fly یا درجا کاربرد داره.

یک کاربرد بسیار خوب از این generator ها استفاده از آنها به عنوان ورودی فانکشن‌هاست. به عنوان مثال:

در مثال بالا اعداد ۱ و ۲ و ۳ و ۴ توسط generator ایجاد و به فانکشن‌ها به عنوان ورودی داده وارد میشن و متدها عملایتشون را روی اون اعداد انجام میدن.

خروجی مثال بالا:

10 4


چرا Generators؟

خوب الان که میدونیم Generatorها در پایتون چی هستند، به این سوال میرسیم که چرا باید از اونا استفاده کرد و چرا بدردمون میخورن؟ (منبع)

اول از همه اینکه پیاده‌سازی اونها راحته. همونطور که توی مثال اول کار دیدید، اگر میخواستیم یک Iterator توسط کلاس ایجاد کنیم دردسرش زیاد بود و واسه همین استفاده از Generator میتونه بهترین و منطقی‌ترین جایگزین باشه.

دوم اینکه Generatorها به شدت Memory Efficient هستند (همونطور که قبلا اشاره کردیم بهش) و به صورت بهینه از مموری استفاده می‌کنند و دلیلش را هم الان میدونیم. دلیلش اینه که در هر بار صدا زدن ()next روی Generator Object یکی از خروجی‌هاش در همون لحظه ایجاد میشه اما توی فانکشن معمولی اینطوری نیست.

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

خروجی مثال بالا ۱ و ۲ و ۳ هست که هر بار ()next روی Generator Object ما صدا زده بشه یک خروجی ساخته میشه و چاپ میشه.

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

توی مثال بالا ما فایل را با with اطلاعاتش را میخونیم، یک generator ایجاد میکنیم که با هر بار صدا زده شدن، یک خط از فایل را میخونه و ستون سوم اونا برمیگردونه پس تا اینجا از ذخیره خیلی از اطلاعات در حافظه پرهیز کردیم. در خط بعد یک generator ایجاد میکنیم که اطلاعات خرید پیتزا ? را به صورت عدد برای ما برمیگردونه. در حقیقت ما هر بار روی per_hour فانکشن ()next را صدا بزنیم، یک خط از فایل خونده می‌شه، و تعداد پیتزای ? فروخته شده در اون خط برگردونده میشه. حالا اگه به فانکشن ()sum بدیم per_hour را، حاصل پیتزا‌های ? فروخته شده برای ما برگردونده خواهد شد.


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


بخش‌های دیگر این مجموعه

- مفاهیمی در پایتون که شاید نمیدانید - بخش اول - Iterators

با تشکر.

برنامه نویسیپایتونpythonیادگیریprogramming
دانشجوی ارشد هوش مصنوعی، برنامه نویس. غرق در کامپیوتر، هوش مصنوعی، تکنولوژی و از این قبیل موضوعات.
شاید از این پست‌ها خوشتان بیاید