مو

مولدها در Python

1 تمرین

درباره‌ی مولدها

یک generator تابعی یا عبارتی است که نوع خاصی از iterator را به اسم generator iterator برمی‌گرداند. Generator-iterators تنبل هستند: مقادیر values خود را در حافظه ذخیره نمی‌کنند، بلکه آن‌ها را هنگام نیاز تولید می‌کنند.

یک تابع generator شبیه هر تابع دیگری است، اما یک یا چند عبارت yield دارد. هر yield اجرای کد را معلق می‌کند و وضعیت فعلی اجرا (شامل همه‌ی متغیرهای محلی و دستورهای try) را ذخیره می‌کند. وقتی generator از سر گرفته می‌شود، وضعیت را از همان نقطه‌ی تعلیق ادامه می‌دهد، برخلاف توابع معمولی که با هر فراخوانی از نو تنظیم می‌شوند.

ساخت یک generator

generatorها تقریباً مثل سایر توابع حلقه‌ای یا بازگشتی ساخته می‌شوند، اما به یک عبارت yield نیاز دارند که کمی بعدتر با جزئیات بررسی می‌کنیم.

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

>>> def squares(list_of_numbers):
...     squares = []
...     for number in list_of_numbers:
...         squares.append(number ** 2)
...     return squares

می‌توانید آن تابع را به این شکل به یک generator تبدیل کنید:

>>> def squares_generator(list_of_numbers):
...     for number in list_of_numbers:
...         yield number ** 2

منطق پشت این کار این است که وقتی به همه‌ی مقادیر یک‌جا نیاز ندارید، از generator استفاده می‌کنید.

این کار حافظه و توان پردازشی را ذخیره می‌کند، چون فقط مقداری که هم‌اکنون روی آن کار می‌کنید محاسبه می‌شود.

استفاده از یک generator

generatorها (از نظر فنی generator-iteratorها هستند؛ به نکته‌ی زیر نگاه کنید.) نوعی iterator هستند و هر جایی در Python که iterator یا iterable انتظار می‌رود می‌توان از آن‌ها استفاده کرد. این شامل توابع یا اشیایی می‌شود که یک iterable/iterator را به عنوان آرگومان می‌خواهند. برای بررسی عمیق‌تر، چگونه در Python یک iterator بسازیم را ببینید.

Note

generator-iteratorها زیرمجموعه‌ی خاصی از iteratorها هستند. Iterators همان سازوکار/پروتکلی هستند که امکان حلقه‌زدن روی iterableها را فراهم می‌کنند. generator-iteratorها و iteratorهایی که iterables رایج Python برمی‌گردانند بسیار شبیه هم رفتار می‌کنند، اما چند تفاوت مهم هست که باید به آن‌ها توجه کرد:

  • آن‌ها به‌صورت تنبل ارزیابی می‌شوند؛ پیمایش یک‌طرفه است و «بازگشت» به مقدار قبلی وجود ندارد.
  • با پیمایش مقادیر برگشتی، مصرف می‌شوند؛ هیچ بازتنظیمی یا ذخیره‌ای در حافظه وجود ندارد.
  • قابل مرتب‌سازی نیستند و نمی‌توان آن‌ها را معکوس کرد.
  • از نوع دنباله نیستند و indexes ندارند. نمی‌توانید با جمع یا تفریق به مقدار قبلی یا بعدی ارجاع دهید و نمی‌توانید از نماد براکت ([]) یا برش استفاده کنید.
  • با تابع len() نمی‌توان از آن‌ها استفاده کرد، چون طولی ندارند.
  • می‌توانند متناهی یا نامتناهی باشند؛ هنگام جمع‌آوری همه‌ی مقادیر از یک generator-iterator نامتناهی احتیاط کنید!

برای استفاده از generator squares_generator():

>>> squared_numbers = squares_generator([1, 2, 3, 4])

>>> for square in squared_numbers:
...     print(square)
...
1
4
9
16

مقادیر داخل یک generator را می‌توان با تابع next() هم تولید کرد یا به آن‌ها دسترسی داشت. next() متد __next__() یک شیء generator را فراخوانی می‌کند و کد generator را تا عبارت yield آن «پیش می‌برد» یا ارزیابی می‌کند، که سپس مقدار را «yield می‌کند» یا برمی‌گرداند.

>>> squared_numbers = squares_generator([1, 2])

>>> next(squared_numbers)
1
>>> next(squared_numbers)
4

وقتی یک generator کاملاً مصرف شود و مقدار دیگری برای برگرداندن نداشته باشد، خطای StopIteration را پرتاب می‌کند.

>>> next(squared_numbers)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

تفاوت میان iterableها و generatorها

generatorها زیرمجموعه‌ی خاصی از iteratorها هستند. Iterators همان سازوکار/پروتکلی هستند که امکان حلقه‌زدن روی iterableها را فراهم می‌کنند. generatorها و iteratorهایی که iterables رایج Python برمی‌گردانند بسیار شبیه هم رفتار می‌کنند، اما چند تفاوت مهم هست که باید به آن‌ها توجه کرد:

  • generatorها یک‌طرفه هستند؛ «بازگشت» به مقدار قبلی وجود ندارد.

  • پیمایش generatorها مقادیر برگشتی را مصرف می‌کند؛ بدون بازتنظیم.

  • generatorها (که به‌صورت تنبل ارزیابی می‌شوند) قابل مرتب‌سازی نیستند و نمی‌توان معکوسشان کرد.

  • generatorها indexes ندارند، بنابراین نمی‌توانید با جمع یا تفریق به مقدار قبلی یا بعدی ارجاع دهید.

  • با تابع len() نمی‌توان از generatorها استفاده کرد.

  • generatorها می‌توانند متناهی یا نامتناهی باشند؛ هنگام جمع‌آوری همه‌ی مقادیر یک generator نامتناهی احتیاط کنید.

عبارت yield

عبارت yield بسیار شبیه عبارت return است.

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

وقتی yield ارزیابی می‌شود، اجرای تابع دربرگیرنده را متوقف می‌کند و مقادیر آن تابع را در همان لحظه برمی‌گرداند.

سپس تابع در محدوده‌ی خود باقی می‌ماند و وقتی __next__() فراخوانی شود، اجرا تا رسیدن دوباره به yield ادامه می‌یابد.

Note

استفاده از عبارت‌های yield بیرون از توابع ممنوع است.

>>> def infinite_sequence():
...     current_number = 0
...     while True:
...         yield current_number
...         current_number += 1

>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1

چرا generator؟

generatorها در بسیاری از کاربردها مفید هستند.

وقتی با مجموعه‌ای بزرگ کار می‌کنید، ممکن است نخواهید همه‌ی مقادیر آن را در memory بگذارید. می‌توان از generator استفاده کرد تا روی داده‌های بزرگ‌تر تکه‌تکه کار کنید و در حافظه صرفه‌جویی کرده و کارایی را بالا ببرید.

generatorها همچنین وقتی یک فرایند یا محاسبه پیچیده، پرهزینه یا نامتناهی باشد بسیار کمک‌کننده‌اند:

>>> def infinite_sequence():
...     current_number = 0
...     while True:
...         yield current_number
...         current_number += 1

حالا هر بار که __next__() روی شیء infinite_sequence فراخوانی شود، عدد قبلی + ۱ را برمی‌گرداند.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود

مولدها را یاد بگیرید

تمرین کردن قفل شده است

برای تمرین مولدها قفل 2 تمرین دیگر را باز کنید