Что делает ключевое слово yield? - PullRequest
9261 голосов
/ 24 октября 2008

Какая польза от ключевого слова 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? Список возвращен? Единственный элемент? Это называется снова? Когда последующие звонки прекратятся?


1. Этот фрагмент кода был написан Йохеном Шульцем (jrschulz), который создал отличную библиотеку Python для метрических пространств. Это ссылка на полный источник: Модуль mspace .

Ответы [ 38 ]

153 голосов
/ 24 октября 2008

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

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

139 голосов
/ 04 апреля 2013

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

Оператор 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)

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

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


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

def f():
  while True:
    yield 4

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

Повторим: продолжения могут сохранять состояние стека программы, а генераторы могут сохранять состояние итерации. Это означает, что продолжения более мощные, чем генераторы, но также и то, что генераторы намного, намного проще. Их легче реализовать для языкового дизайнера, и программисту проще в их использовании (если у вас есть какое-то время, попробуйте прочитать и понять эту страницу о продолжениях и вызвать / 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.

125 голосов
/ 24 октября 2008

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

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

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

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

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

for item in sequence:

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

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

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

110 голосов
/ 04 февраля 2014

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

Чтобы понять, что yield делает в следующем коде, вы можете использовать палец для отслеживания цикла по любому коду, который имеет yield. Каждый раз, когда ваш палец ударяется о yield, вы должны ждать ввода next или send. Когда вызывается next, вы прослеживаете код до тех пор, пока не нажмете yield… код справа от yield оценивается и возвращается вызывающей стороне… затем вы ждете. Когда next вызывается снова, вы выполняете еще один цикл по коду. Однако вы заметите, что в сопрограмме yield также может использоваться с send…, который будет отправлять значение из вызывающего в функцию уступки. Если дано 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()
102 голосов
/ 25 июля 2014

Существует еще одно yield использование и значение (начиная с Python 3.3):

yield from <expr>

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

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

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

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

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

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

88 голосов
/ 14 ноября 2017

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

Полагаю, вы выучили утверждение 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 output:

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

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

  • return и yield - близнецы
  • list и generator - близнецы
86 голосов
/ 04 октября 2012

Вот несколько примеров 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)
82 голосов
/ 28 января 2013

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

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

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

79 голосов
/ 21 августа 2013

С точки зрения программирования итераторы реализованы как thunks .

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

http://en.wikipedia.org/wiki/Message_passing

" next " - это сообщение, отправленное закрытию, созданное вызовом " iter ".

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

Вот демонстрация, которая использует структуру R6RS, но семантика абсолютно идентична Python. Это та же модель вычислений, и для ее переписывания в Python требуется только изменение синтаксиса.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->
72 голосов
/ 20 декабря 2013

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

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

...