Цей пост входить в цикл на основі перекладів популярних питань / відповідей з stackoverflow.
Коли побачиш функцію з ключовим словом yield всередині, то застосуй трюк, описаний нижче, і ти відразу зрозумієш як працює ця функція.
- 1. Спочатку функції додай рядок: result = [].
- 2. Заміни кожен виклик yield на result.append (expr).
- 3. Додай return result в кінець визначення функції.
- 4. Ура! Тепер немає жодного виклику yield - тепер легко зрозуміти що робить функція.
- 5 / Розібрався? Поверни визначення функції в первісний стан.
Цей трюк допоможе тобі зрозуміти ідею позаду функції, а також наочно розкладе логіку її втілення. З однією лише різницею: то що відбувається коли використовується yield сильно відрізняється від способу з використанням списку. Найчастіше підхід з використанням генератора (yield робить зі звичайної функції генератор) буде менш ненажерливим: використання пам'яті буде менше, а швидкість роботи вище. А іноді трюк може ввести виконання функції в нескінченний цикл, незважаючи на те, що оригінальна функція працює як слід. Читай далі, щоб дізнатися більше
Розрізняй ітеріруемие об'єкти, ітератори і генератори
Для початку розберемося з інтерфейсом ітератора. Коли ти пишеш:
Python робить два наступні кроки:
Отримує об'єкт ітератора для списку mylist.
Виклик iter (mylist) повертає об'єкт з методом next () (або __next __ () для Py3).
Використовує отриманий итератор для того, щоб пройти в циклі за елементами списку. Потім for x in mylist. послідовно викликає метод .next (), отриманого на першому кроці ітератора, і значення, яке повертає .next () присвоюється x. Якщо список закінчився, то .next () генерує виняток StopIteration.
Насправді Python проробляє описані кроки щоразу, коли йому потрібно пройтися послідовно по вмісту об'єкта: як за допомогою циклу for, так і в коді виду therlist.extend (mylist)
Ітеріруемие об'єкти
Наш mylist - це ітеріруемий об'єкт, який реалізує протокол ітератора. Якщо тобі потрібно в будь-якому твоєму класі зробити його екземпляри ітеріруемимі, то для цього буде потрібно всього лише реалізувати в ньому метод __iter __ (). Цей метод повинен повертати ітератор. Итератор - це об'єкт з методом .next ().
Поряд з __iter __ () ти можеш визначити для свого класу і метод .next (), тоді __iter __ () буде повертати self. Правда такий варіант підійде тільки для простих випадків. Для більш складних, де потрібно кільком Ітератор проходитися по членам одного і того ж об'єкта, вже не спрацює.
В цілому, це і є протокол ітератора. Багато об'єктів Python його реалізують:
- Вбудовані списки, словники, кортежі, безлічі, файли
- Призначені для користувача класи, що реалізують метод __iter __ ()
- Генератори
Цикл for не знає з яким об'єктом взаємодіє. Цикл просто слід протоколу ітератора і щасливо бере елемент за елементом з кожним новим викликом .next (). Списки повертають послідовно елемент за елементом, словники віддають послідовно ключі, файли - рядок за рядком. А генератори. що ж це те місце, де вступає в гру yield.
Якби я написав замість yield три рази return, то тоді при виклику f123 () спрацював би перший зустрінутий return і функція завершила б свою роботу. Однак, так як ми використовували yield, то функція стає неординарною і коли інтерпретатор досягає yield, то повертається об'єкт генератор (а не обчислюється значення) - потім функція переходить в призупинене стан. Коли, наприклад, коли в цикл for передається об'єкт генератора, то f123 () відновлює роботу, відпрацьовує до наступного зустрінутого yield і повертає таке значення. Це відбувається до тих пір, поки цикл не завершує свою роботу в слідстві того, що генератор видає виключення StopIteration.
Таким чином об'єкт генератора виглядає як деякий адаптер: з одного боку він реалізує протокол ітератора, визначаючи методи __iter __ () і next (), з іншого боку він виконує функцію рівно на стільки скільки потрібно для отримання чергового значення, а потім знову призупиняє її.
Навіщо використовувати генератори
Ти можеш писати код, який не використовує генератори, але реалізує ту ж саму логіку. Наприклад, використовуючи описаний на початку статті трюк з тимчасовим списком. Цей трюк не працюватиме у випадках з нескінченними циклами, занадто великими списками (що тягне надмірне споживання пам'яті)
Інший підхід полягає в написанні якогось призначеного для користувача класу, який зберігає стан між викликами в своїх примірниках і реалізує в них метод .next () (або __next __ () в Py3). Залежно від алгоритму, код всередині методу .next () може бути дуже комплексним, що тягне за собою приховані помилки і побічні результати. Ось тут і засяють генератори, надаючи просте рішення.