python итератор Что делает ключевое слово «yield»?




итератор python (24)

Все отличные ответы, однако немного сложно для новичков.

Я полагаю, вы узнали это returnзаявление.

Как аналогия, returnи yieldблизнецы. returnозначает «возврат и останов», тогда как «доходность» означает «возврат»,

  1. Попробуйте получить num_list return.
def num_list(n):
    for i in range(n):
        return i

Запустить его:

In [5]: num_list(3)
Out[5]: 0

Смотрите, вы получаете только один номер, а не список из них. returnникогда не позволяет вам побеждать счастливо, просто реализует один раз и уходит.

  1. Приходит yield

Заменить returnс yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Теперь вы выигрываете, чтобы получить все цифры.

По сравнению с тем, returnкоторый выполняется один раз и останавливается, yieldвыполняется время, которое вы планируете. Вы можете интерпретировать returnкак return one of them, и yieldкак return all of them. Это называется iterable.

  1. Еще один шаг, который мы можем переписать yieldс помощьюreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

Это главное yield.

Разница между returnвыводами списка и yieldвыходом объекта :

Вы всегда будете получать [0, 1, 2] из объекта списка, но только можете получить их из « yieldвыходного объекта » один раз. Таким образом, он имеет новый generatorобъект имени, отображаемый в Out[11]: <generator object num_list at 0x10327c990>.

В заключение, как метафора, чтобы понять это:

  • returnи yieldявляются близнецами
  • listи generatorявляются близнецами

Каково использование ключевого слова yield в Python? Что оно делает?

Например, я пытаюсь понять этот код 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

И это вызывающий:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Что происходит, когда _get_child_candidates метод _get_child_candidates ? Вернулся ли список? Один элемент? Он снова называется? Когда последующие вызовы прекратятся?

1. Код исходит от Jochen Schulz (jrschulz), который создал отличную библиотеку Python для метрических пространств. Это ссылка на полный источник: Module mspace .


Для тех, кто предпочитает минимальный рабочий пример, медитируйте на этом интерактивном сеансе Python :

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print i
... 
1
2
3
>>> for i in g:
...   print i
... 
>>> # Note that this time nothing was printed

Ключевое слово yield сводится к двум простым фактам:

  1. Если компилятор определяет ключевое слово yield внутри функции, эта функция больше не возвращается через оператор return . Вместо этого он немедленно возвращает ленивый объект «ожидающего списка», называемый генератором
  2. Генератор истребитель. Что такое итерируемый ? Это что-то вроде list или set или range или dict-view со встроенным протоколом для посещения каждого элемента в определенном порядке .

В двух словах: генератор представляет собой ленивый, инкрементно ожидающий список , а операторы yield позволяют использовать нотацию функций для программирования значений списка, которые генератор должен постепенно выплевывать.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

пример

Давайте определим функцию makeRange которая точно так же, как range Python. Вызов makeRange(n) ВОЗВРАЩАЕТ ГЕНЕРАТОР:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Чтобы заставить генератор немедленно вернуть свои ожидающие значения, вы можете передать его в list() (как и любой итерабельный):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

Сравнительный пример с «просто возвратом списка»

Вышеприведенный пример можно рассматривать как просто создание списка, к которому вы добавляете и возвращаете:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Однако есть одна большая разница; см. последний раздел.

Как вы можете использовать генераторы

Итерабельность - это последняя часть понимания списка, и все генераторы являются итерабельными, поэтому их часто используют так:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Чтобы лучше понять генераторы, вы можете играть с модулем itertools (обязательно используйте chain.from_iterable а не chain когда это оправдано). Например, вы даже можете использовать генераторы для реализации бесконечно длинных ленивых списков, таких как itertools.count() . Вы можете реализовать свой собственный def enumerate(iterable): zip(count(), iterable) или, альтернативно, сделать это с ключевым словом yield в цикле while.

Обратите внимание: генераторы действительно могут использоваться для многих других вещей, таких как выполнение сопрограмм или недетерминированное программирование или другие элегантные вещи. Однако представленная здесь точка зрения «ленивых списков» - это наиболее распространенное использование, которое вы найдете.

За кулисами

Вот как работает «Итерационный протокол Python». То есть, что происходит, когда вы делаете list(makeRange(5)) . Это то, что я описал ранее как «ленивый, инкрементный список».

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Встроенная функция next() просто вызывает .next() , которая является частью «протокола итерации» и найдена на всех итераторах. Вы можете вручную использовать функцию next() (и другие части протокола итерации) для реализации причудливых вещей, обычно за счет удобочитаемости, поэтому старайтесь избегать этого ...

мелочи

Обычно большинство людей не заботятся о следующих различиях и, вероятно, хотят перестать читать здесь.

В Python- talk , итерабельным является любой объект, который «понимает концепцию for-loop», как список [1,2,3] , а итератор - это конкретный экземпляр запрошенного цикла for, например [1,2,3].__iter__() . Генератор точно такой же, как и любой итератор, за исключением того, как он был написан (с синтаксисом функций).

Когда вы запрашиваете итератор из списка, он создает новый итератор. Однако, когда вы запрашиваете итератор из итератора (который вы редко делаете), он просто дает вам копию самого себя.

Таким образом, в маловероятном случае, когда вы не можете сделать что-то вроде этого ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... тогда помните, что генератор является итератором ; то есть одноразовое использование. Если вы хотите его повторно использовать, вы должны myRange(...) позвонить myRange(...) . Если вам нужно дважды использовать результат, преобразуйте результат в список и сохраните его в переменной x = list(myRange(5)) . Те, кто абсолютно необходимо клонировать генератор (например, кто делает ужасающее хакерское метапрограммирование), могут использовать itertools.tee если это абсолютно необходимо, поскольку предложение переписываемого итератора Python PEP было отложено.


Вот мысленный образ того, что yieldделает.

Мне нравится думать, что поток имеет стек (даже если он не реализован именно так).

Когда вызывается нормальная функция, она помещает свои локальные переменные в стек, выполняет некоторые вычисления, затем очищает стек и возвращает. Значения его локальных переменных больше никогда не видны.

С помощью yieldфункции, когда ее код начинает работать (т. next()Е. После вызова функции, возвращая объект-генератор, метод которого затем вызывается), он аналогичным образом помещает свои локальные переменные в стек и вычисляет какое-то время. Но затем, когда он обращается к yieldзаявлению, прежде чем очистить свою часть стека и вернуть его, он получает моментальный снимок своих локальных переменных и сохраняет их в объекте-генераторе. Он также записывает место, где он в настоящий момент находится в своем коде (т. Е. Конкретный yieldоператор).

Таким образом, это своего рода замороженная функция, на которую висит генератор.

Когда он next()вызывается впоследствии, он извлекает вещи функции в стек и повторно анимирует их. Функция продолжает вычисляться с того места, где она остановилась, не обращая внимания на то, что она просто провела вечность в холодном хранилище.

Сравните следующие примеры:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Когда мы вызываем вторую функцию, она ведет себя по-разному к первому. yieldЗаявление может быть недоступно, но если он присутствует в любом месте, он изменяет природу того , что мы имеем дело с.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

Вызов yielderFunction()не запускает его код, но делает генератор из кода. (Может быть, неплохо назвать такие вещи yielderпрефиксом для удобочитаемости.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

gi_codeИ gi_frameполе , где замороженное состояние хранится. Изучая их dir(..), мы можем подтвердить, что наша ментальная модель выше заслуживает доверия.


Вот простой yieldподход, чтобы вычислить ряд фибоначчи, объяснил:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Когда вы вводите это в свой REPL, а затем попробуйте и назовите его, вы получите мистифицирующий результат:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Это связано с тем, что присутствие yieldPython сигнализировало, что вы хотите создать генератор , то есть объект, который генерирует значения по требованию.

Итак, как вы генерируете эти значения? Это можно сделать непосредственно, используя встроенную функцию next, или, косвенно, путем подачи ее на конструкцию, которая потребляет значения.

Используя встроенную next()функцию, вы напрямую вызываете .next/ __next__, заставляя генератор генерировать значение:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Косвенно, если вы предоставляете fibв forцикл, listинициализатор, tupleинициализатор или что-либо еще, что ожидает объект, который генерирует / производит значения, вы будете «потреблять» генератор, пока не будет произведено больше значений (и оно возвращается) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Аналогично, с tupleинициализатором:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Генератор отличается от функции в том смысле, что она ленива. Он выполняет это, поддерживая локальное состояние и позволяя вам возобновлять работу, когда вам нужно.

Когда вы сначала вызываете fibего, называя его:

f = fib()

Python компилирует эту функцию, встречает yieldключевое слово и просто возвращает объект-генератор обратно на вас. Не очень полезно.

Когда вы затем запрашиваете, он генерирует первое значение, прямо или косвенно, оно выполняет все найденные им утверждения до тех пор, пока не встретится с a yield, а затем вернет значение, которое вы указали, yieldи приостановит. Для примера, который лучше демонстрирует это, давайте использовать некоторые printвызовы (заменим на print "text"if на Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Теперь введите REPL:

>>> gen = yielder("Hello, yield!")

у вас есть объект-генератор, ожидающий команды для генерации значения. Используйте nextи посмотрите, что получилось:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Неопознанные результаты - это то, что напечатано. Выведенным результатом является то, что возвращается yield. Вызовите еще nextраз:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Генератор помнит, что он был приостановлен yield valueи возобновлен оттуда. Следующее сообщение печатается, и поиск yieldзаявления для паузы на нем выполняется снова (из-за whileцикла).


Существует один тип ответов, который, как мне кажется, пока не дан, среди множества замечательных ответов, описывающих использование генераторов. Вот ответ теории языка программирования:

yieldОператор в Python возвращает генератор. Генератор в Python - это функция, которая возвращает продолжения (и, в частности, тип сопрограммы, но продолжения представляют собой более общий механизм для понимания того, что происходит).

Продолжение теории языков программирования - это гораздо более фундаментальный вид вычислений, но они не часто используются, потому что их очень сложно рассуждать, а также очень сложно реализовать. Но идея о том, что такое продолжение, прост: это состояние вычисления еще не закончено. В этом состоянии сохраняются текущие значения переменных, операции, которые еще предстоит выполнить, и т. Д. Затем в какой-то момент в программе можно вызвать продолжение, так что переменные программы сбрасываются в это состояние и выполняются операции, которые были сохранены.

Продолжения в этой более общей форме могут быть реализованы двумя способами. На самом call/ccделе стек программы буквально сохраняется, а затем, когда вызывается продолжение, стек восстанавливается.

В продолжении стиля прохода (CPS) продолжения - это просто нормальные функции (только в языках, где функции являются первоклассными), которые программист явно управляет и переходит к подпрограммам. В этом стиле состояние программы представлено замыканиями (и переменными, которые в них закодированы), а не переменными, которые находятся где-то в стеке. Функции, управляющие потоком управления, принимают продолжение как аргументы (в некоторых вариантах CPS функции могут принимать множественные продолжения) и манипулировать потоком управления, вызывая их, просто называя их и возвращаясь впоследствии. Очень простой пример продолжения прохождения:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

В этом (очень упрощенном) примере программист сохраняет операцию фактического написания файла в продолжение (что потенциально может быть очень сложной операцией со многими деталями для записи), а затем передает это продолжение (т. Е. закрытие класса) другому оператору, который выполняет некоторую дополнительную обработку, а затем вызывает его, если необходимо. (Я часто использую этот шаблон проектирования в реальном графическом программировании, потому что он экономит мне строки кода или, что более важно, управлять потоком управления после запуска событий графического интерфейса пользователя.)

Остальная часть этого поста без ограничения общности концептуализирует продолжение как CPS, потому что это намного проще понять и прочитать.


Теперь поговорим о генераторах в Python. Генераторы являются определенным подтипом продолжения. В то время как продолжения могут вообще сохранять состояние вычисления (т. Е. Стек вызовов программы), генераторы могут только сохранить состояние итерации по итератору . Хотя это определение несколько вводит в заблуждение для некоторых случаев использования генераторов. Например:

def f():
  while True:
    yield 4

Это явно разумный итерируемый, поведение которого хорошо определено - каждый раз, когда генератор итерации над ним, он возвращает 4 (и делает это навсегда). Но это, вероятно, не прототипный тип итеративного, который приходит на ум при мыслите итераторов (т for x in collection: do_something(x). Е. ). Этот пример иллюстрирует мощность генераторов: если что-то итератор, генератор может сохранить состояние своей итерации.

Повторить: Continuations может сохранять состояние стека программы, а генераторы могут сохранять состояние итерации. Это означает, что продолжения более мощные, чем генераторы, но также и то, что генераторов много, намного проще. Они легче реализовать разработчику языка, и им проще программировать (если у вас есть время для записи, попробуйте прочитать и понять эту страницу о продолжениях и вызовах / cc ).

Но вы можете легко реализовать (и концептуализировать) генераторы как простой, конкретный случай продолжения прохождения стиля:

Всякий раз, когда он yieldвызывается, он сообщает функции возвратить продолжение. Когда функция вызывается снова, она начинается с того места, где она была остановлена. Таким образом, в псевдо-псевдокоде (т. Е. Не псевдокод, но не код) метод генератора в nextосновном выглядит следующим образом:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

где yieldключевое слово на самом деле является синтаксическим сахаром для реальной функции генератора, в основном что-то вроде:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Помните, что это просто псевдокод, и фактическая реализация генераторов в Python более сложна. Но в качестве упражнения, чтобы понять, что происходит, попробуйте использовать стиль продолжения передачи для реализации объектов-генераторов без использования yieldключевого слова.


Он возвращает генератор. Я не особенно знаком с Python, но я считаю, что это те же самые вещи, как итератор C #, если вы знакомы с ними.

Основная идея заключается в том, что компилятор / интерпретатор / что-то делает некоторые обманки, так что в отношении вызывающего абонента они могут продолжать вызов next (), и он будет сохранять возвращаемые значения - как если бы метод генератора был приостановлен . Теперь, очевидно, вы не можете «приостановить» метод, поэтому компилятор строит конечный автомат, чтобы вы могли запомнить, где вы сейчас находитесь и как выглядят локальные переменные и т. Д. Это намного проще, чем писать итератор самостоятельно.


Таким образом, yieldоператор преобразует вашу функцию в фабрику, которая создает специальный объект, называемый a, generatorкоторый обертывает тело оригинальной функции. Когда generatorон повторяется, он выполняет вашу функцию до тех пор, пока не достигнет следующего, а yieldзатем приостановит выполнение и оценит значение, переданное в yield. Он повторяет этот процесс на каждой итерации до тех пор, пока путь выполнения не выйдет из функции. Например,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

просто выходы

one
two
three

Мощность исходит от использования генератора с циклом, который вычисляет последовательность, генератор выполняет цикл, останавливаясь каждый раз, чтобы «дать» следующий результат вычисления, таким образом, он рассчитывает список «на лету», а преимущество - память сохранены для особо больших вычислений

Скажем, вы хотели создать свою собственную rangeфункцию, которая производит итерируемый диапазон чисел, вы можете сделать это так,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

и использовать его так:

for i in myRangeNaive(10):
    print i

Но это неэффективно, потому что

  • Вы создаете массив, который вы используете только один раз (это отнимает память)
  • Этот код на самом деле перебирает этот массив дважды! :(

К счастью, Гвидо и его команда были достаточно щедры, чтобы развить генераторы, чтобы мы могли просто сделать это;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Теперь на каждой итерации функция на генераторе называется next()исполнением функции до тех пор, пока он не достигнет оператора 'yield', в котором он остановится и «даст» значение или достигнет конца функции. В этом случае при первом вызове next()выполняется до оператора yield и выводится 'n', при следующем вызове он будет выполнять оператор increment, возвращаться к 'while', оценивать его, и если true, он останавливается и снова выведите 'n', он будет продолжать этот путь до тех пор, пока условие while не вернет false, и генератор перейдет в конец функции.


Подумайте об этом так:

Итератор - просто причудливый зондирующий термин для объекта, который имеет следующий () метод. Таким образом, функция yield-ed оказывается примерно такой:

Оригинальная версия:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Это в основном то, что делает интерпретатор Python с указанным выше кодом:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Для более глубокого понимания того, что происходит за кулисами, цикл for можно переписать следующим образом:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Означает ли это больше смысла или просто путает вас больше? :)

Я должен отметить, что это упрощение для иллюстративных целей. :)


В то время как много ответов показывают, почему вы должны использовать a yieldдля создания генератора, для этого больше пользы yield. Это довольно просто сделать сопрограмму, которая позволяет передавать информацию между двумя блоками кода. Я не буду повторять ни одного из прекрасных примеров, которые уже были даны об использовании yieldдля создания генератора.

Чтобы понять, что yieldделает в следующем коде, вы можете использовать свой палец для отслеживания цикла через любой код, который имеет yield. Каждый раз, когда ваш палец попадает на него yield, вы должны ждать ввода nextили sendввода. Когда nextвызывается, вы прослеживаете код до тех пор, пока не нажмете yield... код справа от yieldнего оценивается и возвращается вызывающему абоненту ... тогда вы ждете. Когда nextвызывается снова, вы выполняете другой цикл через код. Тем не менее, вы заметите, что в сопрограмме yieldтакже можно использовать с send..., который отправит значение от вызывающего в функцию yielding. Еслиsend дано, тоyieldполучает отправленное значение и выплевывает его с левой стороны ... тогда трассировка по коду прогрессирует, пока вы не нажмете yieldснова (возвращая значение в конце, как если бы оно nextбыло вызвано).

Например:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

Многие люди используют, returnа не yield, но в некоторых случаях yieldмогут быть более эффективными и удобными в работе.

Вот пример, который yieldопределенно лучше всего подходит для:

return (в функции)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

выход (в функции)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Функции вызова

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Обе функции выполняют то же самое, но yieldиспользуют три строки вместо пяти и имеют одну меньшую переменную, о которой нужно беспокоиться.

Это результат кода:

Как вы можете видеть, обе функции выполняют одно и то же. Единственное различие - return_dates()это список и yield_dates()дает генератор.

Пример реальной жизни - это что-то вроде чтения файла по строкам или если вы просто хотите создать генератор.


Доходность - это объект

А returnв функции возвращает одно значение.

Если вы хотите, чтобы функция возвращала огромный набор значений , используйте yield.

Что еще более важно, yieldэто барьер .

как барьер на языке CUDA, он не будет передавать управление, пока оно не будет завершено.

То есть, он будет запускать код в вашей функции с самого начала, пока он не ударит yield. Затем он вернет первое значение цикла.

Затем каждый другой вызов будет запускать цикл, который вы написали в функции еще раз, возвращая следующее значение, пока не будет возвращено какое-либо значение.


Выход дает вам генератор.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Как вы можете видеть, в первом случае foo одновременно сохраняет весь список в памяти. Это не большое дело для списка с 5 элементами, но что, если вы хотите список из 5 миллионов? Мало того, что это огромный eater памяти, он также требует много времени, чтобы построить в то время, когда функция вызывается. Во втором случае бар просто дает вам генератор. Генератор является итерируемым, что означает, что вы можете использовать его в цикле for и т. Д., Но каждое значение может быть доступно только один раз. Все значения также не сохраняются в памяти одновременно; объект-генератор «запоминает», где он был в цикле в последний раз, когда вы его назвали, - таким образом, если вы используете итерируемый (скажем) счет до 50 миллиардов, вам не нужно считать до 50 миллиардов всех сразу и хранить 50 миллиардов номеров для подсчета. Опять же, это довольно надуманный пример,вы, вероятно, будете использовать itertools, если вы действительно хотите считать до 50 миллиардов. :)

Это самый простой вариант использования генераторов. Как вы сказали, его можно использовать для написания эффективных перестановок, используя выход, чтобы проталкивать вещи через стек вызовов вместо использования какой-то переменной стека. Генераторы также могут использоваться для специализированного обхода дерева и любых других вещей.


Есть еще одна вещь: функция, которая дает, на самом деле не должна заканчиваться. Я написал код следующим образом:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Тогда я могу использовать его в другом коде:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Это действительно помогает упростить некоторые проблемы и облегчает работу.


Вот простой пример:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Выход:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Я не разработчик Python, но, похоже, я yieldзанимаю позицию потока программы, а следующий цикл начинается с позиции «yield». Похоже, что он ждет в этой позиции, и только перед этим, возвращая ценность снаружи, и в следующий раз продолжает работать.

Кажется, это интересная и приятная способность: D


yieldкак элемент возврата для функции. Разница в том, что yieldэлемент превращает функцию в генератор. Генератор ведет себя точно так же, как функция, пока что-то не «уступило». Генератор останавливается до тех пор, пока он не будет вызван следующим образом, и продолжит с той же точки, что и он. Вы можете получить последовательность всех «полученных» значений в одном, вызвав list(generator()).


Вот пример на простом языке. Я предоставлю соответствие между концепциями высокого уровня человека и концепциями Python низкого уровня.

Я хочу работать с последовательностью чисел, но я не хочу беспокоить себя созданием этой последовательности, я хочу только сосредоточиться на операции, которую я хочу сделать. Итак, я делаю следующее:

  • Я звоню вам и говорю вам, что я хочу последовательность чисел, которая создается определенным образом, и я даю вам знать, что такое алгоритм.
    Этот шаг соответствует defвведению функции генератора, т. Е. Функции, содержащей a yield.
  • Некоторое время спустя я говорю вам: «Хорошо, приготовьтесь рассказать мне последовательность чисел».
    Этот шаг соответствует вызову функции-генератора, которая возвращает объект-генератор. Обратите внимание, что вы еще не говорите мне никаких номеров; вы просто хватаете свою бумагу и карандаш.
  • Я спрашиваю вас: «Скажите мне следующий номер», и вы скажете мне первый номер; после этого вы ждете меня, чтобы спросить вас о следующем номере. Это ваша работа, чтобы помнить, где вы были, какие цифры вы уже сказали, и каков следующий номер. Меня не интересуют детали.
    Этот шаг соответствует вызову .next()объекта-генератора.
  • ... повторить предыдущий шаг, пока ...
  • в конце концов, вы можете подойти к концу. Вы не говорите мне номер; вы просто кричите: «Держите лошадей! Я закончил! Больше никаких номеров!»
    Этот шаг соответствует объекту-генератору, заканчивающему его задание, и сбою StopIterationисключения. Функция генератора не требует повышения исключения. Он автоматически поднимается, когда функция заканчивается или выдает a return.

Это то, что делает генератор (функция, содержащая a yield); он начинает выполнение, приостанавливается всякий раз, когда он делает a yield, и когда его запрашивают .next()значение, оно продолжается с момента последнего. Он идеально подходит по дизайну с протоколом итератора Python, который описывает, как последовательно запрашивать значения.

Самый известный пользователь протокола итератора - это forкоманда в Python. Итак, всякий раз, когда вы делаете:

for item in sequence:

не имеет значения, sequenceесть ли список, строка, словарь или объект- генератор, как описано выше; результат один и тот же: вы читаете элементы из последовательности один за другим.

Обратите внимание, что defиспользование функции, которая содержит yieldключевое слово, не единственный способ создать генератор; это просто самый простой способ создать его.

Для получения более точной информации читайте о типах итераторов , инструкции yield и generators в документации Python.


Ярлык для yield Grokking

Когда вы увидите функцию с предложениями yield , примените этот простой трюк, чтобы понять, что произойдет:

  1. Вставьте результат строки result = [] в начале функции.
  2. Замените каждый yield expr на result.append(expr) .
  3. Вставьте результат return result линии в нижней части функции.
  4. Yay - больше никаких заявлений о yield ! Прочитайте и определите код.
  5. Сравните функцию с исходным определением.

Этот трюк может дать вам представление о логике функции, но то, что на самом деле происходит с yield , существенно отличается от того, что происходит в подходе, основанном на списках. Во многих случаях подход к доходности будет намного более эффективным с точки зрения памяти и быстрее. В других случаях этот трюк заставит вас застрять в бесконечном цикле, хотя оригинальная функция работает просто отлично. Читайте дальше, чтобы узнать больше...

Не путайте ваши итераторы, итераторы и генераторы

Во-первых, протокол итератора - когда вы пишете

for x in mylist:
    ...loop body...

Python выполняет следующие два действия:

  1. Получает итератор для mylist :

    Вызов iter(mylist) -> возвращает объект с помощью метода next() (или __next__() в Python 3).

    [Это тот шаг, о котором многие люди забывают рассказать вам]

  2. Использует итератор для перебора элементов:

    Продолжайте вызывать метод next() на итераторе, возвращенный с шага 1. Возвращаемое значение из next() присваивается x и тело цикла выполняется. Если исключение StopIteration возникает изнутри next() , это означает, что в итераторе больше нет значений, и цикл завершен.

Истина заключается в том, что Python выполняет вышеупомянутые два шага в любое время, когда хочет перебрать содержимое объекта, поэтому он может быть циклом for, но он также может быть кодом типа otherlist.extend(mylist) (где otherlist является списком Python) ,

Здесь mylist является итерабельным, поскольку он реализует протокол итератора. В пользовательском классе вы можете реализовать метод __iter__() чтобы сделать экземпляры вашего класса итерабельными. Этот метод должен возвращать итератор . Итератором является объект со next() методом next() . Можно реализовать как __iter__() и next() в одном классе, и __iter__() возвращает self . Это будет работать для простых случаев, но не тогда, когда вы хотите, чтобы два итератора переходили по одному и тому же объекту одновременно.

Итак, это протокол итератора, многие объекты реализуют этот протокол:

  1. Встроенные списки, словари, кортежи, наборы, файлы.
  2. Определенные пользователем классы, которые реализуют __iter__() .
  3. Генераторы.

Обратите внимание, что цикл for не знает, с каким объектом он имеет дело - он просто следует за протоколом итератора и с удовольствием получает элемент после элемента, поскольку он вызывает next() . Встроенные списки возвращают свои элементы по одному, словари возвращают ключи один за другим, файлы возвращают строки один за другим и т. Д. И генераторы возвращаются ... ну вот где yield приходит:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Вместо операторов yield , если у вас было три оператора return в f123() только первый будет выполнен, и функция выйдет. Но f123() является обычной функцией. Когда f123() , он не возвращает никаких значений в f123() yield! Он возвращает объект-генератор. Кроме того, функция действительно не выходит - она ​​переходит в приостановленное состояние. Когда цикл for пытается перебрать объект-генератор, функция возвращается из своего приостановленного состояния в самой следующей строке после возвращаемого ранее результата, выполняет следующую строку кода, в данном случае инструкцию yield и возвращает это как следующий пункт. Это происходит до тех пор, пока функция не выйдет, и в этот момент генератор вызывает StopIteration , и цикл выходит.

Таким образом, объект-генератор подобен адаптеру - на одном конце он демонстрирует протокол итератора, подвергая __iter__() и next() чтобы поддерживать цикл for обратном порядке. На другом конце, однако, он выполняет функцию достаточно, чтобы получить из нее следующее значение, и возвращает ее в режим ожидания.

Зачем использовать генераторы?

Обычно вы можете написать код, который не использует генераторы, но реализует ту же логику. Один из вариантов заключается в использовании временного списка «трюк», о котором я упоминал ранее. Это не будет работать во всех случаях, например, если у вас бесконечные циклы, или это может привести к неэффективному использованию памяти, когда у вас действительно длинный список. Другой подход заключается в реализации нового итерируемого класса SomethingIter который сохраняет состояние в членах экземпляра и выполняет следующий логический шаг в next() (или __next__() в методе Python 3). В зависимости от логики код внутри метода next() может оказаться очень сложным и подверженным ошибкам. Здесь генераторы обеспечивают простое и чистое решение.


Вот некоторые примеры Python о том, как реально реализовать генераторы, как будто Python не предоставил для них синтаксический сахар:

Как генератор Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Использование лексических замыканий вместо генераторов

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Использование закрытий объектов вместо генераторов (поскольку ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

Существует другое yieldиспользование и смысл (с Python 3.3):

yield from <expr>

От PEP 380 - Синтаксис для делегирования в подгенератор :

Для генератора предлагается синтаксис для делегирования части своих операций другому генератору. Это позволяет разделять фрагмент кода, содержащий «выход», и помещать его в другой генератор. Кроме того, подгенератор может возвращаться со значением, и значение становится доступным для генератора делегирования.

Новый синтаксис также открывает некоторые возможности для оптимизации, когда один генератор повторно дает значения, полученные другим.

Более того, this представит (начиная с Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

чтобы избежать совпадения сопрограмм с обычным генератором (сегодня yieldиспользуется в обоих случаях).


Что делает ключевое слово yield в Python?

Ответить

  • Функция с yield при вызове возвращает Generator .
  • Генераторы - это итераторы, потому что они реализуют протокол итератора , поэтому вы можете перебирать их.
  • Генератор также может быть отправлен информацией , что делает его концептуально сопрограммой .
  • В Python 3 вы можете делегировать от одного генератора другому в обоих направлениях с yield from .
  • (Приложение критикует пару ответов, включая верхнюю, и обсуждает использование return в генераторе.)

Генераторы:

yield является законным внутри определения функции, а включение yield в определение функции заставляет его возвращать генератор.

Идея для генераторов исходит из других языков (см. Сноску 1) с различными реализациями. В генераторах Python выполнение кода frozen в точке выхода. Когда генератор вызывается (методы обсуждаются ниже), выполнение возобновляется, а затем зависает при следующем выходе.

yield обеспечивает простой способ реализации протокола итератора , определяемый следующими двумя способами: __iter__ и next (Python 2) или __next__ (Python 3). Оба эти метода делают объект итератором, который вы можете вводить с помощью базового класса Iterator базового модуля.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> 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.

Тип генератора является подтипом итератора:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

И, если необходимо, мы можем ввести тип:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Особенностью Iterator является то, что после исчерпания вы не можете повторно использовать или перезагружать его:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Вам нужно будет сделать еще один, если вы хотите снова использовать его функциональность (см. Сноску 2):

>>> list(func())
['I am', 'a generator!']

Можно программно выдавать данные, например:

def func(an_iterable):
    for item in an_iterable:
        yield item

Вышеуказанный простой генератор также эквивалентен приведенному ниже - как и Python 3.3 (и не доступен в Python 2), вы можете использовать yield from :

def func(an_iterable):
    yield from an_iterable

Тем не менее, yield from также позволяет делегировать подгенераторы, что будет объяснено в следующем разделе о совместном делегировании с суб-сопрограммами.

Сопрограммы:

yield формирует выражение, которое позволяет передавать данные в генератор (см. сноску 3)

Вот пример, обратите внимание на received переменную, которая будет указывать на данные, которые отправляются генератору:

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)

Во-первых, мы должны поставить очередь генератора со встроенной функцией, next . Он будет вызывать соответствующий next или __next__ метод, в зависимости от используемой версии Python:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

И теперь мы можем отправлять данные в генератор. ( Отправка None - это то же самое, что и вызов next .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Совместная делегация в субкорутине с yield from

Теперь напомним, что yield from доступен в Python 3. Это позволяет нам делегировать сопрограммы к подкроуту:

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:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


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)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Вы можете больше узнать о точной семантике yield from PEP 380.

Другие методы: закрыть и бросить

Метод close повышает значение GeneratorExit в момент, когда выполнение функции было заморожено. Это также __del__ поэтому вы можете поместить любой код очистки, где вы обрабатываете GeneratorExit :

>>> my_account.close()

Вы также можете генерировать исключение, которое может быть обработано в генераторе или передано обратно пользователю:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Заключение

По-моему, я затронул все аспекты следующего вопроса:

Что делает ключевое слово yield в Python?

Оказывается, yield очень много. Я уверен, что могу добавить к этому еще более подробные примеры. Если вы хотите больше или иметь конструктивную критику, сообщите мне, комментируя ниже.

Приложение:

Критика верхнего / принятого ответа **

  • Он смущен тем, что делает итерабельным , просто используя список в качестве примера. См. Мои ссылки выше, но в итоге: в iterable есть метод __iter__ возвращающий итератор . Итератор предоставляет .next (Python 2 или .__next__ (Python 3), который неявно вызывается for циклов до тех пор, пока он не StopIteration , и как только он это сделает, он будет продолжать это делать.
  • Затем он использует выражение генератора для описания того, что такое генератор. Поскольку генератор - это просто удобный способ создания итератора , он только смущает вопрос, и мы до сих пор еще не дошли до yield .
  • В управлении исчерпанием генератора он вызывает метод .next , а вместо этого он должен использовать встроенную функцию, next . Это будет подходящий слой косвенности, потому что его код не работает в Python 3.
  • Itertools? Это не имело никакого отношения к тому, что yield .
  • Никакое обсуждение методов, которые yield дает наряду с новой функциональностью yield from в Python 3. Верхний / принятый ответ - очень неполный ответ.

Критика ответа, предполагающая yield в выражении или понимании генератора.

В настоящее время грамматика допускает любое выражение в понимании списка.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Поскольку yield является выражением, некоторые из них были заинтересованы в том, чтобы использовать его в понимании или выражении генератора - несмотря на отсутствие особого примера использования.

Основные разработчики CPython обсуждают вопрос о снижении его надбавки . Вот сообщение из списка рассылки:

30 января 2017 года в 19:05 Бретт Кэннон писал:

На солнце, 29 января 2017 года в 16:39 Крейг Родригес писал:

Я в порядке с любым подходом. Оставляя вещи так, как они есть на Python 3, это нехорошо, ИМХО.

Мое голосование - это SyntaxError, поскольку вы не получаете того, чего ожидаете от синтаксиса.

Я бы согласился, что это разумное место для нас, чтобы закончить, поскольку любой код, основанный на текущем поведении, действительно слишком умный, чтобы его можно было обслуживать.

Что касается получения там, мы, скорее всего, захотим:

  • Синтаксис Предупреждение или отказ
  • Предупреждение Py3k в 2.7.x
  • SyntaxError в 3.8

Привет, Ник.

- Ник Коглан | ncoghlan на gmail.com | Брисбен, Австралия

Кроме того, есть выдающаяся проблема (10544), которая, по-видимому, указывает на то, что это никогда не является хорошей идеей (PyPy, реализация Python, написанная на Python, уже повышает предупреждения синтаксиса.)

Итог, пока разработчики CPython не скажут нам об ином: не добавляйте yield в выражении генератора или понимании.

Оператор return в генераторе

В Python 2 :

В функции-генераторе оператор return не может включать expression_list . В этом контексте голый return указывает на то, что генератор выполнен и вызовет StopIteration .

В expression_listосновном это любое число выражений, разделенных запятыми - по существу, в Python 2 вы можете остановить генератор с помощьюreturn , но вы не можете вернуть значение.

В Python 3 :

В функции генератора returnоператор указывает, что генератор выполнен и будет StopIterationвызван. Возвращаемое значение (если оно есть) используется как аргумент для построения StopIterationи становитсяStopIteration.value атрибутом.

Сноски

  1. Языки CLU, Sather и Icon были указаны в предложении о внедрении концепции генераторов в Python. Общая идея заключается в том, что функция может поддерживать внутреннее состояние и предоставлять пользователю промежуточные точки данных по запросу. Это обещало быть превосходным по производительности другим подходам, включая потоки Python , которые даже не доступны в некоторых системах.

  2. Это означает, например, что xrangeобъекты ( rangeв Python 3) не являются Iterators, хотя они итерабельны, потому что они могут быть повторно использованы. Как и списки, их __iter__методы возвращают объекты итератора.

  3. yieldбыл первоначально представлен как оператор, что означает, что он может появляться только в начале строки в кодовом блоке. Теперь yieldсоздается выражение yield. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Это изменение было предложено, чтобы позволить пользователю отправлять данные в генератор так, как это можно было бы получить. Чтобы отправлять данные, нужно иметь возможность назначить их чему-то, и для этого утверждение просто не будет работать.


Еще один TL; DR

Итератор в списке : next()возвращает следующий элемент списка

Генератор итератора : next()вычислит следующий элемент «на лету» (код выполнения)

Вы можете увидеть выход / генератор как способ вручную запустить поток управления извне (например, продолжить цикл на один шаг), вызвав next, однако, сложный поток.

Примечание . Генератор НЕ является нормальной функцией. Он запоминает предыдущее состояние, например, локальные переменные (стек). См. Другие ответы или статьи для подробного объяснения. Генератор можно повторить только один раз . Вы могли обойтись yield, но это было бы не так хорошо, поэтому его можно было бы считать «очень хорошим» языковым сахаром.


Я собирался опубликовать «прочитанное на странице 19« Питона Python: Essential Reference »для быстрого описания генераторов», но многие другие уже опубликовали хорошие описания.

Кроме того, обратите внимание, что yieldих можно использовать в сопрограммах как двойное их использование в функциях генератора. Хотя это не то же самое, что и ваш фрагмент кода, он (yield)может использоваться как выражение в функции. Когда вызывающий абонент отправляет значение методу, используя этот send()метод, сопрограмма будет выполняться до тех пор, пока не (yield)будет встречен следующий оператор.

Генераторы и сопрограммы - отличный способ настроить приложения типа потока данных. Я думал, что было бы полезно узнать о другом использовании yieldутверждения в функциях.


yieldточно так же return- он возвращает все, что вы рассказываете (как генератор). Разница в том, что при следующем вызове генератора выполнение начинается с последнего вызова yieldоператора. В отличие от возврата, кадр стека не очищается, когда происходит выход, однако управление передается обратно вызывающему, поэтому его состояние возобновится при следующей функции.

В случае вашего кода функция get_child_candidatesдействует как итератор, поэтому, когда вы расширяете свой список, он добавляет по одному элементу за раз в новый список.

list.extendвызывает итератор, пока он не исчерпан. В случае образца кода, который вы отправили, было бы намного яснее просто вернуть кортеж и добавить его в список.





coroutine