Что делает ключевое слово 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 ]

59 голосов
/ 14 июня 2013

Вот мысленный образ того, что делает 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(..), мы можем подтвердить, что наша ментальная модель, описанная выше, заслуживает доверия.

50 голосов
/ 29 июля 2015

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

def getNextLines():
   while con.isOpen():
       yield con.read()

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

for line in getNextLines():
    doSomeThing(line)

Выполнение управления переводом получено

Управление выполнением будет передано из getNextLines () в цикл for при выполнении yield. Таким образом, каждый раз, когда вызывается getNextLines (), выполнение начинается с того места, где оно было приостановлено в последний раз.

Таким образом, вкратце, функция со следующим кодом

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

напечатает

"first time"
"second time"
"third time"
"Now some useful value 12"
44 голосов
/ 25 марта 2016

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

Когда yield используется вместо return в функции Python, эта функция превращается в нечто особенное, называемое generator function. Эта функция вернет объект типа generator. Ключевое слово yield - это флаг, который уведомляет компилятор python о специальной обработке такой функции. Нормальные функции завершаются, когда из них возвращается какое-то значение. Но с помощью компилятора функцию генератора можно считать возобновляемой. Таким образом, контекст выполнения будет восстановлен, и выполнение будет продолжено с последнего запуска. Пока вы явно не вызовете return, что вызовет исключение StopIteration (которое также является частью протокола итератора), или не достигнет конца функции. Я нашел много ссылок о generator, но этот один из functional programming perspective является наиболее усваиваемым.

(Теперь я хочу поговорить об обосновании generator и о iterator, основанном на моем собственном понимании. Надеюсь, это поможет вам понять существенную мотивацию итератора и генератора. Такая концепция проявляется и в других языках, таких как C #.)

Как я понимаю, когда мы хотим обработать кучу данных, мы обычно сначала храним данные где-то, а затем обрабатываем их одну за другой. Но этот наивный подход проблематичен. Если объем данных огромен, заранее хранить их в целом дорого. Таким образом, вместо непосредственного хранения самого data, почему бы не сохранить какое-то косвенное значение metadata, то есть the logic how the data is computed.

Существует два подхода к переносу таких метаданных.

  1. ОО подход, мы заключаем метаданные as a class. Это так называемый iterator, который реализует протокол итератора (то есть методы __next__() и __iter__()). Это также часто встречающийся шаблон проектирования итератора .
  2. Функциональный подход, мы оборачиваем метаданные as a function. Это так называемый generator function. Но под капотом возвращенный итератор generator object все еще IS-A, поскольку он также реализует протокол итератора.

В любом случае, создается итератор, то есть какой-то объект, который может дать вам нужные данные. Подход ОО может быть немного сложным. В любом случае, какой из них использовать - решать только вам.

44 голосов
/ 01 сентября 2015

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

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

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

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

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

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

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

43 голосов
/ 13 октября 2016

Таким образом, оператор yield преобразует вашу функцию в фабрику, которая создает специальный объект с именем 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', при следующем вызове он выполняет оператор приращения, возвращается к 'while', оценивает его и, если оно истинно, остановится и снова выдаст 'n', так будет продолжаться до тех пор, пока условие while не вернет false и генератор не перейдет к концу функции.

41 голосов
/ 10 сентября 2016

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

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

возврат (в функции)

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 использует три строки вместо пяти и имеет на одну переменную меньше беспокойства.

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

Output

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

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

36 голосов
/ 20 мая 2015

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

35 голосов
/ 18 ноября 2015

Ключевое слово yield просто собирает возвращаемые результаты. Думайте о yield как return +=

32 голосов
/ 20 февраля 2016

Вот простой 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>

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

Итак, как вы генерируете эти значения? Это можно сделать либо напрямую, используя встроенную функцию 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 и просто возвращает объект генератора обратно к вам. Не очень полезно, кажется.

Когда вы затем запрашиваете, он генерирует первое значение, прямо или косвенно, он выполняет все найденные операторы, пока не встретит yield, затем возвращает значение, которое вы указали для yield, и делает паузу. Для примера, который лучше демонстрирует это, давайте использовать несколько вызовов print (замените на print "text", если на 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).

31 голосов
/ 02 января 2017

Простой пример, чтобы понять, что это такое: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print i

Вывод:

1 2 1 2 1 2 1 2
...