یکبار برای همیشه ـ‌ Iterators

تصویر Iteratorها در طبیعت
تصویر Iteratorها در طبیعت

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

مقدمه

عکس بالا یک کوآلا رو میبینم که از بومی‌های استرالیاست و معروفِ به تنبلی؛ میگن نه تنها روزی بیست ساعت می‌خوابه، بلکه یه مقدار برگ هم توی لپ‌ش قایم می‌کنه واسه میان‌وعده! نکته جالب اینجاس که Iterator و generatorها هم یه همچین نقشی توی پایتون دارن. بازم مثل همیشه یک سری کلمات کلیدی داریم که اول اونارو معرفی می‌کنیم تا ادبیات همه‌مون برای ادامه‌ی کار یکی بشه.

  • کلمه Iteration: هر موقع شما با یک داده‌ای طرف باشید که شامل چندین مقدار باشه و هر دفعه یکی از مقدارهاشو بگیرید دارید iteration می‌کنید. مثل حلقه‌ها که هربار یک مقدار از یک داده‌ی رو به شما میده فرض کنید یک فایل رو باز کردید و هربار یک خط از اون رو می‌خونید.
  • کلمه Iterable: روی هر چیزی نمیشه iteration کرد، توی پایتون شما روی چیزی می‌تونید Iterate کنید که یکی یا هر دوی این متد‌ها رو داشته باشه: __iter__ و __getitem__. به هر آبجکتی که این ویژگی‌ها رو داشته باشه بهش می‌گیم Iterable چون می‌تونیم روش حلقه بذاریم هر بار یک آیتم رو بگیرم تا به آخرین آیتم برسیم.
  • کلمه Iterator: اصل جنسِ! بدون در نظر گرفتن تعریفش، هر آبجکتی که متد __next__ داشته باشه Iteratorهست. مثال:‌

چون متد __next__ رو داره پس می‌تونیم تابع next رو روی اون صدا بزنیم تا مقدار بعدی رو به ما بده.

 یک نکته  هر آبجکتی که متد __iter__ داشته باشه یعنی قابلیت تبدیل شدن به Iterator رو داره و اگه بخوایم تبدیلش کنیم به Iterator می‌تونیم به یکی از دو روش پایین عمل کنیم: 

توی خط آخر منظور من این بوده که خودِ numbers چون Iterator نیست پس صدا زدن next روی اون بی معنیِ. اما اینکه چرا پس ما میتونیم حلقه بذاریم و هربار یک ایتمش رو بگیریم به خاطر متد __getitem__ هست.

انگیزه ما از یادگیری: Iteratorها و فامیل خیلی نزدیکش Generatorها توی خیلی از بخش‌های پایتون به کار رفتن من جمله:

  • for loops
  • looping over a text file
  • list, dict and set comprehensions
  • built-in functions like filter, zip, map, ...
  • tuple unpacking and etc.

تا اینجای کار بیشتر شبیه کلاس درس فلسفه شد و هدف من این بود که به یک حسی از Iteratorها و کلمات مهم این حوزه برسیم اما بریم سراغ اصل کار که عینِ آدم اینارو معرفی کنیم و بگیم کجاها کاربرد دارن.




بخش اول - Generator

خب، Generatorها در حقیقت خیلی شبیه توابع معمولی هستند با این تفاوت که دیگه از return استفاده نمی‌کنیم و به جاش از yield (بخونید ییِلد ر.ک به لینک) استفاده می‌کنیم.

فرض کنیم یه تابع داریم به اسم countdown که شروع می‌کنه می‌شماره تا به صفر می‌رسه. این رو توی دوحالت Generator و تابع معمولی تعریف می‌کنیم:‌

بریم از این دو تا تابع استفاده کنیم ببینیم چه فرق‌هایی دارند:

تابع اول به محض فراخوانی تمام کارش رو انجام داده و خروجی رو داخل متغییر numbers ریخته. برعکس اون تابع دوم انگار هیچ کاری نکرده فقط انگار یک آبجکتی به نام Generator برگردونده:‌

  • تابع next یک Generator (یا یک Iterator) رو به عنوان ورودی می‌گیره و مقدار بعدی اون رو بر می‌گردونه. این کار با کمک yield صورت می‌گیره. تابع تا اون‌جایی اجرا میشه که به yield میرسه و همون‌جا متوقف میشه. دفعه بعدی که next رو صدا می‌زنیم، دوباره همه کار‌هارو انجام میده تا دوباره به yield میرسه. این کار اینقدر تکرار میشه تا دیگه چیزی برای yield کردن نمی‌مونه و اون موقع است که خطای StopIteration میده.

نکات مهم:‌

  • چیزی که هست Generator دقیقاً مشابه کوآلا داره عمل می‌کنه با تنبل‌ترین شکل ممکن داره کار می‌کنه. اگه دقت کرده باشید برخلاف یک تابع معمولی، این شکل از توابع داره وضعیت رو بخاطر می‌سپاره (به قول فرنگیا Resumable function)،‌ هربار که صداش می‌کنیم دوباره از اول اجرا نمی‌شه بلکه ادامه کارش رو اجرا می‌کنه.
  • خیلی memory efficient هست چون در هر لحظه یک خروجی بر می‌گردونه در صورتی که شکل تابع اول اگه محاسبات بالایی داشته باشیم باید صبر کنیم تا کل کار تموم شه و بعد به خروجی دسترسی داشته باشیم.

بعضی از دوستان معتقدند که استفاده از Iterator و Generator باعث میشه کد ما خواناتر بشه. برای توضیح دادن این بخش فرض کنیم قصد داریم که تابع سری فیبوناچی رو به صورت Generator پیاده‌سازی کنیم.

حالا اگه یکی به ما گفت مثلا من ده عدد اول سری فیبوناچی رو می‌خوام چی؟ تو حالت اول که سریع می‌ریم داخل تابع یک متغییر تعریف می‌کنیم و میشماریم اگه ده شد بزن بیرون. اما این راه‌حل خیلی قشنگ نیست، اگه بخوایم فانکشنال فکر کنیم می‌گیم همین حالت که هست باشه، با ابزار دیگه این کار رو انجام بدیم. اینجاست که یک ماژول خیلی باحال به اسم itertools به کمک ما میاد( اگه دنبال مثال بیشتری برای این بخش هستید هر تابعِ داخل itertools یک مثال هست).

  • مثال اول:‌ده تا عدد اول فیبوناچی
نکته: هر موقع بخواهید به همه مقادیر یک Generator یا Iterator دسترسی داشته باشید کافیه مشابه مثال بالا اون رو تبدیل به list کنید.
  • مثال ۲: اعداد سری فیبوناچی را بدست بیاریم که که بزرگتر از صد هستند

تابع dropwhile دو ورودی می‌گیره یک شرط پیش‌بینی هست و دومی iterable آبجکت.

  • مثال ۳: برعکس‌اش هم هست، اونایی که کمتر از صد هستند رو بده:‌

این ماژول خیلی چیز‌های باحالی داره مثلا اگه می‌خواهید تمام حالتهای ترکیب چند کاراکتر رو بگیرید و هر بار به یک آیتم دسترسی داشته باشید میتونید از permutations کمک بگیرید. یک تابع خیلی باحال به اسم product داره که حتماً بهش نگاه بندازید.

  • آخرین مثال برای تاکید بیشتر! هدف استفاده از Generator باید و باید در راستای کاهش مصرف منابع باشه، وقتی که با چیز‌های بزرگ از حیث منابع سروکار داریم. فرض کنیم قصد داریم یک حمله Brute-Force اجرا کنیم که رمز از سه رقم تشکیل شده. در حالت عادی این برنامه چیزی شبیه پایین میشه:

انتظار ما این‌ هست که با پیدا کردن عدد مورد نظر برنامه متوقف بشه ولی خب این کار صورت نمی‌گیره و حلقه‌ها تا آخرین حالت ممکن اجرا میشن( حتما این بخش رو خودتون بنویسید و اجرا کنید)،‌ این در صورتی هست که با Generatorها مطمئنیم به محض اینکه عدد مورد نظر پیدا شد دیگه محاسبات بیهوده نداریم.

اما Iteratorها!

خُب، اگه یادتون باشه گفتیم Generator یک شکل خیلی خیلی ساده شده‌ی Iteratorها هستند. دقیقاً مشابه این هست که داریم یک کار رو از صفرِ صفر شروع می‌کنیم (Build from scratch). مثال سری فیبوناچی رو یادتون هست که با Generator نوشتیم؟ این بار می‌ریم اون رو با یک تغییر کوچیک به سبک Iterator بنویسیم و می‌فهیم منظورمون از Build from scratch چیه؟

اولین تغییری که به چشم میاد،‌ استفاده از کلاس هست به جای مثال‌های قبلی که همگی تابع بودند. و دو تابع خاص منظوره رو استفاده کردیم که عبارتند از :‌ ‍‍__iter__ و __next__.

  • تابع __iter__ در حقیقت وقتی صدا زده میشه که ما تابع iter روی این یک آبجکت از این کلاس صدا بزنیم. شما ممکنه بگید که ما این‌کار رو تا الان نکردیم؛ حق با شماست. این کار معمولاً خودکار انجام میشه. مثلاً وقتی دارید از حلقه استفاده می‌کنید خود حلقه for این کار رو انجام میده.
  • متد __next__ هم وقتی صدا زده میشه که تابع next روی اون اجرا کنیم.
  • اگه دقت کنید میبینید که دیگه خبری از yield نیست! حتی StopIteration رو هم خودمون تعریف کردیم که این خطا رو بده! فکر کنم با مثال خیلی راحت‌تر باشه این حرفا:

حالا فکر کنم بیشتر به قدرت yield پی‌ بردیم، ساخت یک Iterator با Generator بسیار راحت‌تر میشه. نکته کلیدی قدرت سفارشی‌سازی Iterator است در مقابل سادگیِ Generator و resource-friendly بودن این دوتا دوست عزیز ما در پایتون!

جمع بندی‌

امیدوارم این قسمت یک‌بار برای همیشه حق مطلب رو بخوبی ادا کرده باشه. اگه کسی رو می‌شناسید که دوست داره پایتون رو به صورت حرفه‌ای یاد بگیره یادتون نره این پست‌ها رو باهاش به اشتراک بذارید. خیلی ممنون تا قسمت بعدی یک‌بار برای همیشه خدانگهدار!

از اینجا کجا بریم

اگر بازم حس می‌کنید نیاز به مطالعه بیشتر در این زمینه دارید:

http://www.diveintopython3.net/generators.html

http://www.diveintopython3.net/iterators.html

* اگر از itertools خوشتون اومده حتما این لینک رو هم دنبال کنید کلی مثال جذاب داره!

https://realpython.com/python-itertools/