generator یک تابع یا عبارت است که نوع خاصی از تکرارگر را برمیگرداند که به آن generator iterator میگویند.
Generator-iteratorها تنبل هستند: values خود را در حافظه ذخیره نمیکنند، بلکه مقادیرشان را هنگام نیاز تولید میکنند.
یک تابع مولد شبیه هر تابع دیگری است، اما شامل یک یا چند عبارت yield است.
هر yield اجرای کد را معلق میکند و وضعیت اجرای جاری را ذخیره میکند (شامل همهی متغیرهای محلی و دستورهای try).
وقتی مولد ادامه پیدا میکند، وضعیت را از همان نقطهی تعلیق برمیدارد، برخلاف توابع معمولی که با هر بار فراخوانی از نو آغاز میشوند.
مولدها تقریباً مانند دیگر توابع حلقهای یا بازگشتی ساخته میشوند، اما به یک عبارت yield نیاز دارند که کمی بعد با جزئیات بیشتری به آن میپردازیم.
یک نمونه، تابعی است که مربعهای اعداد یک فهرست مشخص را برمیگرداند. همانطور که اکنون نوشته شده است، باید همهی ورودی پردازش شود تا بتوان هر مقداری را برگرداند:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
میتوانید آن تابع را به این شکل به یک مولد تبدیل کنید:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
دلیلش این است که از مولد زمانی استفاده میکنید که نیازی ندارید همهی مقادیر را یکجا تولید کنید. این کار حافظه و توان پردازشی را ذخیره میکند، چون فقط مقداری محاسبه میشود که در همان لحظه روی آن کار میکنید.
در Python میتوان از مولدها بهجای بیشتر iterables استفاده کرد.
این شامل تابعها یا شیءهایی میشود که یک iterable/iterator را بهعنوان آرگومان میخواهند.
برای استفاده از مولد 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__() یک شیء مولد-تکرارگر را فراخوانی میکند، کد را تا عبارت yield آن «به جلو میبرد» یا ارزیابی میکند و بعد آن مقدار را «بازمیگرداند» یا برمیگرداند:
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
وقتی یک generator-iterator کاملاً مصرف شود و دیگر مقداری برای برگرداندن نداشته باشد، یک خطای StopIteration پرتاب میکند.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
مولد-تکرارگرها زیرمجموعهای خاص از تکرارگرها هستند.
Iterators سازوکار/پروتکلی هستند که حلقهزدن روی تکرارپذیرها را ممکن میکنند.
مولد-تکرارگرها و تکرارگرهایی که iterables رایج Python برمیگردانند رفتار بسیار مشابهی دارند، اما چند تفاوت مهم وجود دارد که باید در نظر بگیرید:
indexes ندارند.
نمیتوانید به مقدار قبلی یا بعدی با جمع یا تفریق ارجاع دهید و نمیتوانید از نگارش کروشه ([]) یا برش استفاده کنید.len() به کار برد، چون طولی ندارند.generator-iterator نامتناهی احتیاط کنید!yield
عبارت yield بسیار شبیه عبارت return است.
برخلاف عبارت return، دستور yield مقادیر را در یک نقطهی مشخص به فراخوانکننده واگذار میکند و ارزیابی یا برگرداندن مقادیر بیشتر را تا زمانی که درخواست شوند معلق نگه میدارد.
وقتی yield ارزیابی میشود، اجرای تابع دربرگیرنده را موقتاً متوقف میکند و مقادیر تابع را در همان نقطهی زمانی برمیگرداند.
پس از آن، تابع در حوزهی خود باقی میماند و وقتی __next__() فراخوانی شود، اجرا تا رسیدن دوباره به yield ادامه پیدا میکند.
استفاده از عبارتهای 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
مولدها در بسیاری از کاربردها مفیدند.
وقتی با مجموعهی بالقوهای بزرگ از مقادیر کار میکنید، ممکن است نخواهید همهی آنها را در حافظه قرار دهید. میتوان از یک مولد برای کار تکهتکه روی دادههای بزرگتر استفاده کرد، که حافظه را ذخیره میکند و کارایی را بهبود میبخشد.
مولدها همچنین وقتی یک فرایند یا محاسبه پیچیده، پرهزینه یا نامتناهی باشد بسیار کمککنندهاند:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
حالا هر بار که __next__() روی شیء infinite_sequence فراخوانی شود، عدد قبلی را بهعلاوهی ۱ برمیگرداند.
Conda Airlines بزرگترین شرکت هواپیمایی دنیای برنامهنویسی است و روزانه بیش از ۱۰٬۰۰۰ پرواز دارد!
آنها در حال حاضر همهی صندلیها را دستی به مسافران تخصیص میدهند؛ این کار باید خودکار شود.
آنها از شما خواستهاند نرمافزاری بسازید که تخصیص صندلی مسافران را خودکار کند. آنها میخواهند نرمافزار شما هم از نظر حافظه بهینه باشد و هم سریع.
Conda میخواهد حروف صندلی برای هواپیماهایش تولید کند.
هر هواپیما از ردیفهایی از صندلی تشکیل شده است.
هر ردیف ۴ صندلی دارد.
صندلیهای هر ردیف همیشه با نامهای A، B، C و D مشخص میشوند.
اولین صندلی ردیف A است، دومین صندلی ردیف B است و به همین ترتیب.
پس از رسیدن به D، باید دوباره از A شروع شود.
تابعی به اسم generate_seat_letters(<number>) پیادهسازی کنید که یک int میگیرد که مشخص میکند چند حرف صندلی باید تولید شود.
سپس تابع باید یک تکرارپذیر از حروف صندلی برگرداند.
>>> letters = generate_seat_letters(4)
>>> next(letters)
"A"
>>> next(letters)
"B"
Conda سیستمی میخواهد که بتواند تعداد مشخصی صندلی برای هواپیماهایش تولید کند.
هر هواپیما در هر ردیف ۴ صندلی دارد.
ردیفها با اعداد مشخص میشوند، از 1 شروع میشوند و بالا میروند.
صندلیها باید مرتب باشند، به این شکل: 1A، 1B، 1C، 1D، 2A، 2B، 2C، 2D، 3A، 3B، 3C، 3D، ...
این هم یک مثال:
| x | 1 | 2 |
|---|---|---|
| ردیف | 5 | 21 |
| حرف صندلی | A | D |
| نتیجه | 5A | 21D |
بسیاری از شرکتهای هواپیمایی به خاطر خرافات میان مسافران، در پروازهایشان ردیف شماره ۱۳ را ندارند. Conda Airlines هم این رسم را رعایت میکند، پس مطمئن شوید که برای ردیف شماره ۱۳ صندلی تولید نکنید.
تابعی به اسم generate_seats(<number>) پیادهسازی کنید که یک int میگیرد که مشخص میکند چند صندلی باید تولید شود.
سپس تابع باید یک تکرارپذیر از صندلیهای تولیدشده برگرداند.
>>> seats = generate_seats(10)
>>> next(seats)
"1A"
>>> next(seats)
"1B"
حالا که تابعی دارید که صندلی تولید میکند، میتوانید از آن برای تخصیص صندلی به مسافران استفاده کنید.
تابعی به اسم assign_seats(<passengers>) پیادهسازی کنید که یک list از اسمهای مسافران میگیرد.
سپس تابع باید یک دیکشنری برگرداند که passenger در آن کلید و seat_number مقدار است.
>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob']
>>> assign_seats(passengers)
{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'}
Conda Airlines میخواهد هر بلیت یک کد یکتا داشته باشد. چون شرکت هواپیمایی بزرگی است، پروازهای زیادی دارد. یعنی چندین پرواز با شمارهی صندلی یکسان وجود دارد. آنها میخواهند سیستمی بسازید که بلیتی یکتا بسازد که رشتهای ۱۲ کاراکتری است.
این کد با assigned_seat شروع میشود و بعد از آن flight_id میآید.
بقیهی کد با 0s پر میشود.
تابعی به اسم generate_codes(<seat_numbers>, <flight_id>) پیادهسازی کنید که یک list از seat_numbers و یک string شامل شمارهی پرواز میگیرد.
سپس تابع باید یک generator برگرداند که ticket_number تولید میکند.
>>> seat_numbers = ['1A', '17D']
>>> flight_id = 'CO1234'
>>> ticket_ids = generate_codes(seat_numbers, flight_id)
>>> next(ticket_ids)
'1ACO12340000'
>>> next(ticket_ids)
'17DCO1234000'