Что такое «сопрограмма» в Python?
Многим новичкам, изучающим Python, сложно осмыслить PEP 380. Меня обычно спрашивают:
- Что делает ключевое слово «yield»?
- В каких ситуациях полезно использовать «yield from»?
- Каков классический вариант использования?
- Почему его сравнивают с микропотоками?
Чтобы понять, что делает
yield
, вы должны понимать, что такое генераторы. И прежде чем вы сможете понять генераторы, вы должны понять итерации.
Итерируемые объекты
Итерируемый - это объект, у которого есть __iter__
метод, который возвращает итератор, или который определяет __getitem__
метод, который может принимать последовательные индексы, начиная с нуля (и поднимает IndexError
, когда индексы больше не действительны). Итак, итератор - это объект, из которого вы можете получить итератор.
- все, что можно зациклить (то есть вы можете зациклить строку или файл) или
- все, что может появиться справа от цикла for:
for x in iterable: ...
или - все, что вы можете вызвать с помощью
iter()
, которое вернет ИТЕРАТОР:iter(obj)
>>> mylist = [1, 2, 3]
>>> # mylist = [x*x for x in range(3)] # or a list comprehension
>>> for i in mylist:
... print(i)
1
2
3
Генераторы
Генераторы - это итераторы, своего рода итерации, которые можно выполнять только один раз. Генераторы не хранят все значения в памяти, они генерируют значения на лету:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
Это то же самое, за исключением того, что вы использовали ()
вместо []
. НО вы не можете выполнить for i in mygenerator
второй раз, поскольку генераторы можно использовать только один раз: они вычисляют 0, затем забывают об этом и вычисляют 1, и заканчивают вычисление 4, один за другим.
Урожай
yield
- ключевое слово, которое используется как return
, за исключением того, что функция вернет генератор.
>>> def func(): # a function with yield
... yield 'I am' # is still a function
... yield 'a generator!'
...
>>> gen = func()
>>> type(gen) # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__') # that's an iterable
True
>>> hasattr(gen, 'next') # and with .next (.__next__ in Python 3)
True # implements the iterator protocol.
Это бесполезный пример, но он удобен, когда вы знаете, что ваша функция вернет огромный набор значений, которые вам нужно будет прочитать только один раз.
Чтобы освоить yield
, вы должны понимать, что при вызове функции код, который вы написали в теле функции, не запускается. Функция возвращает только объект генератора, это немного сложно :-)
В первый раз, когда for
вызывает объект-генератор, созданный из вашей функции, он будет запускать код в вашей функции с самого начала до тех пор, пока не достигнет yield
, а затем вернет первое значение цикла. Затем каждый последующий вызов будет запускать другую итерацию цикла, который вы написали в функции, и возвращать следующее значение. Это будет продолжаться до тех пор, пока генератор не будет считаться пустым, что происходит, когда функция выполняется без нажатия yield
. Это может быть из-за того, что цикл подошел к концу, или из-за того, что вы больше не удовлетворяете "if/else"
.
>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]
Вам придется сделать еще один, если вы хотите снова использовать его функции:
>>> list(func())
['I am', 'a generator!']
- Функция с
yield
при вызове возвращает Генератор. - Генераторы являются итераторами, потому что они реализуют протокол итератора, поэтому вы можете перебирать их.
yield
разрешен только внутри определения функции, а включениеyield
в определение функции приводит к возврату генератора.
yield
обеспечивает простой способ реализации протокола итератора, определяемый следующими двумя методами: __iter__
и next
(Python 2) или __next__
(Python 3). Оба этих метода делают объект итератором, который можно проверить с помощью абстрактного базового класса Iterator
из модуля collections
.
Ключевое слово yield
сводится к двум простым фактам:
- Если компилятор обнаруживает ключевое слово
yield
где-нибудь внутри функции, эта функция больше не возвращается с помощью оператораreturn
. Вместо он немедленно возвращает ленивый объект «ожидающий список», называемый генератором. - Генератор повторяется. Что такое итерация? Это что-то вроде
list
,set
,range
или dict-view, со встроенным протоколом для посещения каждого элемента в определенном порядке.
В двух словах: генератор - это ленивый, постепенно ожидающий список, а
yield
операторы позволяют вам использовать нотацию функций для программирования значений списка, которые генератор должен постепенно выдавать.
Сопрограммы
yield
формирует выражение, которое позволяет отправлять данные в генератор.
def bank_account(deposited, interest_rate): while True: calculated_interest = interest_rate * deposited received = yield calculated_interest if received: deposited += received my_account = bank_account(1000, .05)
first_year_interest = next(my_account) # 50.0 next_year_interest = my_account.send(first_year_interest + 1000) # 102.5
Совместное делегирование суб-сопрограммы
def money_manager(expected_rate): under_management = yield # must receive deposited value while True: try: additional_investment = yield expected_rate * under_management if additional_investment: under_management += additional_investment except GeneratorExit: # ignored finally: # ignored
def investment_account(deposited, manager): """ very simple model of an investment account that delegates to a manager """ next(manager) # must queue up manager manager.send(deposited) while True: try: yield from manager except GeneratorExit: return manager.close()
my_manager = money_manager(.06) my_account = investment_account(1000, my_manager)
first_year_return = next(my_account) # 60.0 next_year_return = my_account.send(first_year_return + 1000) # 123.6
Концепция, что yield from generator
эквивалентно for value in generator: yield value
, даже не начинает отдавать должное тому, о чем идет речь yield from
. Потому что, давайте посмотрим правде в глаза, если все, что делает yield from
, это расширяет цикл for
, тогда это не гарантирует добавления yield from
к языку и исключает реализацию целого ряда новых функций в Python 2.x.
yield from
делает это устанавливает прозрачное двунаправленное соединение между вызывающим и вспомогательным генератором:
- Соединение «прозрачно» в том смысле, что оно также будет правильно распространять все, а не только генерируемые элементы (например, распространяются исключения).
- Соединение является «двунаправленным» в том смысле, что данные могут быть отправлены как от, так и к генератору.
Почему его сравнивают с микропотоками?
Я думаю, что этот раздел в PEP говорит о том, что у каждого генератора действительно есть свой собственный изолированный контекст выполнения. Вместе с тем фактом, что выполнение переключается между генератором-итератором и вызывающим с помощью yield
и __next__()
соответственно, это похоже на потоки, где операционная система время от времени переключает выполняющийся поток вместе с контекстом выполнения (стек, регистры, ...).
Эффект от этого также сравним: и генератор-итератор, и вызывающая программа переходят в состояние выполнения одновременно, их выполнение чередуется. Например, если генератор выполняет какие-то вычисления, а вызывающий распечатывает результаты, вы увидите результаты, как только они станут доступны. Это форма параллелизма.
Однако эта аналогия не имеет ничего общего с yield from
- это скорее общее свойство генераторов в Python.
Ошибка в Python «yield»:
Примечание. Это была ошибка в обработке CPython
yield
в интерпретациях и выражениях генератора, исправленная в Python 3.8, с предупреждением об устаревании в Python 3.7. См. Отчет об ошибке Python и записи Что нового для Python 3.7 и Python 3.8.
>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]
Это кажется странным:
- что понимание списка возвращает генератор, а не список
- и что выражение генератора, преобразованное в список, и соответствующее понимание списка содержат разные значения.
Выражения генератора, а также выражения set и dict компилируются в объекты функции (генератора). В Python 3 понимание списков обрабатывается точно так же; По сути, все они представляют собой новую вложенную область видимости.
Вы можете увидеть это, если попытаетесь дизассемблировать выражение генератора:
>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
1 0 LOAD_CONST 0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
3 LOAD_CONST 1 ('<genexpr>')
6 MAKE_FUNCTION 0
9 LOAD_NAME 0 (range)
12 LOAD_CONST 2 (3)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 POP_TOP
23 LOAD_CONST 3 (None)
26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 11 (to 17)
6 STORE_FAST 1 (i)
9 LOAD_FAST 1 (i)
12 YIELD_VALUE
13 POP_TOP
14 JUMP_ABSOLUTE 3
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
Выше показано, что выражение генератора компилируется в объект кода, загруженный как функция (MAKE_FUNCTION
создает объект функции из объекта кода). Ссылка .co_consts[0]
позволяет нам увидеть объект кода, сгенерированный для выражения, и он использует YIELD_VALUE
так же, как функция генератора.
Таким образом, выражение yield
работает в этом контексте, поскольку компилятор рассматривает их как скрытые функции.
Это ошибка; yield
нет места в этих выражениях. грамматика Python до того, как Python 3.7 допускает это (поэтому код компилируемый), но yield
выражение_спецификация показывает, что использование yield
здесь на самом деле не должно работать:
Выражение yield используется только при определении функции-генератора и, следовательно, может использоваться только в теле определения функции.
Это было подтверждено как ошибка в проблеме 10544. Решение ошибки состоит в том, что использование yield
и yield from
поднимет SyntaxError
в Python 3.8; в Python 3.7 он выдает DeprecationWarning
, чтобы гарантировать, что код перестанет использовать эту конструкцию. Вы увидите такое же предупреждение в Python 2.7.15 и выше, если вы используете -3
переключатель командной строки, включающий предупреждения совместимости Python 3.
Ленивый метод чтения большого файла в Python
Чтобы написать ленивую функцию, просто используйте yield
:
def read_in_chunks(file_object, chunk_size=1024): """ Lazy function (generator) to read a file piece by piece. Default chunk size: 1k. """ while True: data = file_object.read(chunk_size) if not data: break yield data
with open('really_big_file.dat') as f: for piece in read_in_chunks(f): process_data(piece)
Для чего вы можете использовать функцию генератора?
Генераторы дают вам ленивую оценку. Вы используете их, перебирая их, либо явно с помощью «for», либо неявно, передавая его в любую функцию или конструкцию, которая выполняет итерацию.
Генераторы хороши для вычисления больших наборов результатов (в частности, вычислений с участием самих циклов), когда вы не знаете, понадобятся ли вам все результаты, или где вы не хотите выделять память для всех результатов одновременно. .
Еще одно применение генераторов (что на самом деле то же самое) - замена обратных вызовов итерацией. В некоторых ситуациях вы хотите, чтобы функция выполняла много работы и иногда отчитывалась перед вызывающим. Традиционно для этого использовалась функция обратного вызова. Вы передаете этот обратный вызов рабочей функции, и она будет периодически вызывать этот обратный вызов. Подход с генератором состоит в том, что рабочая функция (теперь генератор) ничего не знает о обратном вызове и просто уступает, когда хочет что-то сообщить. Вызывающий, вместо того, чтобы писать отдельный обратный вызов и передавать его рабочей функции, выполняет всю работу по созданию отчетов в небольшом цикле «for» вокруг генератора.
Например, предположим, что вы написали программу «поиска файловой системы». Вы можете выполнить поиск полностью, собрать результаты и затем отображать их по одному. Все результаты нужно будет собрать до того, как вы покажете первый, и все результаты будут в памяти одновременно. Или вы можете отображать результаты по мере их нахождения, что было бы более эффективным с точки зрения памяти и гораздо более дружелюбным по отношению к пользователю. Последнее можно сделать, передав функцию печати результатов функции поиска файловой системы, или просто сделав функцию поиска генератором и перебирая результат.
Если вы хотите увидеть пример двух последних подходов, см. Os.path.walk () (старая функция обхода файловой системы с обратным вызовом) и os.walk () (новый генератор обхода файловой системы). Конечно, если вы действительно хотели собрать все результаты в список, подход генератора тривиально преобразовать в подход большого списка:
big_list = list(the_generator)
Когда не следует использовать функцию генератора
Используйте список вместо генератора, когда:
Вам необходимо получить доступ к данным несколько раз (т.е. кешировать результаты, а не пересчитывать их):
for i in outer: # used once, okay to be a generator
for j in inner: # used multiple times, reuse a list
...
Вам потребуется произвольный доступ (или любой доступ, кроме прямого последовательного порядка):
for i in reversed(data): ... # generators aren't reversible
s[i], s[j] = s[j], s[i] # generators aren't indexable
Вам необходимо объединить строки (для чего требуется два прохода данных):
s = ''.join(data) # lists are faster than generators in this use case
Вы используете PyPy, который иногда не может оптимизировать код генератора настолько, насколько это возможно, с помощью обычных вызовов функций и манипуляций со списками.
Надеюсь, вам понравился рассказ! Больше в моем профиле.
Прочтите мой предыдущий рассказ: Прогнозирование ветвей - все, что вам нужно знать.
Посетите мое портфолио: https://harshal.one.