O que a palavra-chave "yield" faz?

10195

Qual é o uso da yieldpalavra - chave em Python e o que ela faz?

Por exemplo, estou tentando entender esse código 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  

E este é o chamador:

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

O que acontece quando o método _get_child_candidatesé chamado? Uma lista é retornada? Um único elemento? É chamado de novo? Quando as chamadas subsequentes serão interrompidas?


1. Este trecho de código foi escrito por Jochen Schulz (jrschulz), que criou uma excelente biblioteca Python para espaços métricos. Este é o link para a fonte completa: Module mspace .

Alex. S.
fonte

Respostas:

14648

Para entender o que yieldfaz, você deve entender o que são geradores . E antes que você possa entender geradores, você deve entender iterables .

Iterables

Ao criar uma lista, você pode ler seus itens um por um. A leitura de seus itens um por um é chamada iteração:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylisté um iterável . Ao usar uma compreensão de lista, você cria uma lista e, portanto, é iterável:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Tudo o que você pode usar " for... in..." é iterável; lists, stringsarquivos ...

Essas iteráveis ​​são úteis porque você pode lê-las quantas vezes quiser, mas armazena todos os valores na memória e isso nem sempre é o que você deseja quando possui muitos valores.

Geradores

Geradores são iteradores, um tipo de iterável que você pode repetir apenas uma vez . Os geradores não armazenam todos os valores na memória, eles geram os valores rapidamente :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

É o mesmo, exceto que você usou em ()vez de []. MAS, você não pode executar for i in mygeneratoruma segunda vez, uma vez que os geradores podem ser usados ​​apenas uma vez: eles calculam 0, esquecem-no e calculam 1 e terminam o cálculo 4, um por um.

Produção

yieldé uma palavra-chave usada como return, exceto que a função retornará um gerador.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Aqui está um exemplo inútil, mas é útil quando você sabe que sua função retornará um enorme conjunto de valores que você precisará ler apenas uma vez.

Para dominar yield, você deve entender que, quando você chama a função, o código que você escreveu no corpo da função não é executado. A função retorna apenas o objeto gerador, isso é um pouco complicado :-)

Em seguida, seu código continuará de onde parou sempre que forusar o gerador.

Agora a parte mais difícil:

A primeira vez que forchamar o objeto gerador criado a partir da sua função, ele executará o código na sua função desde o início até que acerte yield, e retornará o primeiro valor do loop. Em seguida, cada chamada subsequente executará outra iteração do loop que você escreveu na função e retornará o próximo valor. Isso continuará até que o gerador seja considerado vazio, o que acontece quando a função é executada sem bater yield. Isso pode ser porque o loop chegou ao fim ou porque você não satisfaz mais um "if/else".


Seu código explicado

Gerador:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Chamador:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Este código contém várias partes inteligentes:

  • O loop itera em uma lista, mas a lista se expande enquanto o loop está sendo iterado :-) É uma maneira concisa de passar por todos esses dados aninhados, mesmo que seja um pouco perigoso, pois você pode acabar com um loop infinito. Nesse caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))esgote todos os valores do gerador, mas whilecontinue criando novos objetos geradores que produzirão valores diferentes dos anteriores, pois não são aplicados no mesmo nó.

  • O extend()método é um método de objeto de lista que espera uma iterável e adiciona seus valores à lista.

Geralmente passamos uma lista para ele:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Mas no seu código, ele obtém um gerador, o que é bom porque:

  1. Você não precisa ler os valores duas vezes.
  2. Você pode ter muitos filhos e não os quer armazenados na memória.

E funciona porque o Python não se importa se o argumento de um método é uma lista ou não. O Python espera iterables, para que ele funcione com strings, listas, tuplas e geradores! Isso é chamado de digitação de pato e é uma das razões pelas quais o Python é tão legal. Mas isso é outra história, para outra pergunta ...

Você pode parar por aqui ou ler um pouco para ver um uso avançado de um gerador:

Controlando a exaustão do gerador

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Nota: Para Python 3, use print(corner_street_atm.__next__())ouprint(next(corner_street_atm))

Pode ser útil para várias coisas, como controlar o acesso a um recurso.

Itertools, seu melhor amigo

O módulo itertools contém funções especiais para manipular iteráveis. Já desejou duplicar um gerador? Cadeia de dois geradores? Agrupar valores em uma lista aninhada com uma linha única? Map / Zipsem criar outra lista?

Então apenas import itertools.

Um exemplo? Vamos ver as possíveis ordens de chegada para uma corrida de quatro cavalos:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Entendendo os mecanismos internos da iteração

A iteração é um processo que implica iteráveis ​​(implementando o __iter__()método) e iteradores (implementando o __next__()método). Iteráveis ​​são quaisquer objetos dos quais você pode obter um iterador. Iteradores são objetos que permitem iterar em iterables.

Há mais sobre isso neste artigo sobre como os forloops funcionam .

e-satis
fonte
355
yieldnão é tão mágico que esta resposta sugere. Quando você chama uma função que contém uma yieldinstrução em qualquer lugar, obtém um objeto gerador, mas nenhum código é executado. Então, toda vez que você extrai um objeto do gerador, o Python executa o código na função até chegar a uma yieldinstrução, pausa e entrega o objeto. Quando você extrai outro objeto, o Python continua logo após o yielde continua até chegar a outro yield(geralmente o mesmo, mas uma iteração posteriormente). Isso continua até que a função termine no final, momento em que o gerador é considerado esgotado.
Matthias Fripp
30
"Essas iteráveis ​​são úteis ... mas você armazena todos os valores na memória e isso nem sempre é o que você deseja", é errado ou confuso. Um iterável retorna um iterador ao chamar iter () no iterável, e nem sempre um iterador precisa armazenar seus valores na memória, dependendo da implementação do método iter , ele também pode gerar valores na sequência sob demanda.
picmate涅
Seria bom acrescentar a essa ótima resposta por que é a mesma coisa, exceto que você usou em ()vez de[] , especificamente o que ()é (pode haver confusão com uma tupla).
WoJ
Posso estar errado, mas um gerador não é um iterador, um "gerador chamado" é um iterador.
aderchox
@MatthiasFripp "Isso continua até que a função termine" - ou encontra uma returndeclaração. ( returné permitido em uma função que contenha yield, desde que não especifique um valor de retorno.)
alaniwi
2007

Atalho para o entendimento yield

Quando você yieldvir uma função com instruções, aplique este truque fácil para entender o que acontecerá:

  1. Inserir uma linha result = []no início da função.
  2. Substitua cada um yield exprpor result.append(expr).
  3. Insira uma linha return resultna parte inferior da função.
  4. Yay - não há mais yielddeclarações! Leia e descubra o código.
  5. Compare a função com a definição original.

Esse truque pode fornecer uma idéia da lógica por trás da função, mas o que realmente acontece yieldé significativamente diferente do que acontece na abordagem baseada em lista. Em muitos casos, a abordagem de rendimento também será muito mais eficiente em termos de memória e mais rápida. Em outros casos, esse truque o deixará preso em um loop infinito, mesmo que a função original funcione perfeitamente. Continue lendo para saber mais ...

Não confunda seus iteráveis, iteradores e geradores

Primeiro, o protocolo iterador - quando você escreve

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

O Python executa as duas etapas a seguir:

  1. Obtém um iterador para mylist:

    Call iter(mylist)-> retorna um objeto com um next()método (ou __next__()no Python 3).

    [Este é o passo que a maioria das pessoas esquece de contar]

  2. Usa o iterador para fazer um loop sobre itens:

    Continue chamando o next()método no iterador retornado da etapa 1. O valor de retorno de next()é atribuído a xe o corpo do loop é executado. Se uma exceção StopIterationé gerada de dentro next(), significa que não há mais valores no iterador e o loop é encerrado.

A verdade é que o Python executa as duas etapas acima sempre que quiser fazer um loop sobre o conteúdo de um objeto - então pode ser um loop for, mas também pode ser semelhante ao código otherlist.extend(mylist)(onde otherlistestá uma lista do Python).

Aqui mylistestá um iterável porque implementa o protocolo iterador. Em uma classe definida pelo usuário, você pode implementar o __iter__()método para tornar as instâncias da sua classe iteráveis. Este método deve retornar um iterador . Um iterador é um objeto com um next()método É possível implementar ambos __iter__()e next()na mesma classe e ter __iter__()retorno self. Isso funcionará em casos simples, mas não quando você desejar dois iteradores fazendo loop no mesmo objeto ao mesmo tempo.

Portanto, esse é o protocolo do iterador, muitos objetos implementam esse protocolo:

  1. Listas internas, dicionários, tuplas, conjuntos, arquivos.
  2. Classes definidas pelo usuário que implementam __iter__().
  3. Geradores.

Observe que um forloop não sabe com que tipo de objeto está lidando - apenas segue o protocolo do iterador e fica feliz em obter item após item, conforme chama next(). As listas internas retornam seus itens um por um, os dicionários retornam as chaves uma a uma, os arquivos retornam as linhas uma a uma etc. E os geradores retornam ... bem, é aí que yieldentra:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Em vez de yieldinstruções, se você tivesse três returninstruções, f123()apenas a primeira seria executada e a função sairia. Mas f123()não é uma função comum. Quando f123()é chamado, ele não retorna nenhum dos valores nas declarações de rendimento! Retorna um objeto gerador. Além disso, a função não sai realmente - entra em um estado suspenso. Quando o forloop tenta fazer um loop sobre o objeto gerador, a função retoma de seu estado suspenso na linha seguinte após a yieldvolta anterior, executa a próxima linha de código, nesse caso, uma yieldinstrução e retorna como a próxima item. Isso acontece até a função sair, quando o gerador aumenta StopIteratione o loop sai.

Portanto, o objeto gerador é como um adaptador - em uma extremidade, ele exibe o protocolo iterador, expondo __iter__()e next()métodos para manter o forloop feliz. No outro extremo, no entanto, ele executa a função apenas o suficiente para obter o próximo valor e o coloca novamente no modo suspenso.

Por que usar geradores?

Geralmente, você pode escrever um código que não use geradores, mas implemente a mesma lógica. Uma opção é usar o 'truque' temporário da lista que mencionei antes. Isso não funcionará em todos os casos, por exemplo, se você tiver loops infinitos, ou pode fazer uso ineficiente da memória quando você tiver uma lista realmente longa. A outra abordagem é implementar uma nova classe iterável SomethingIter que mantém o estado nos membros da instância e executa a próxima etapa lógica no método next()(ou __next__()no Python 3). Dependendo da lógica, o código dentro do next()método pode acabar parecendo muito complexo e propenso a erros. Aqui, os geradores fornecem uma solução limpa e fácil.

user28409
fonte
20
"Quando você vê uma função com instruções de rendimento, aplique esse truque fácil para entender o que acontecerá" Isso não ignora completamente o fato de que você pode sendentrar em um gerador, que é uma grande parte do objetivo dos geradores?
DanielSank
10
"pode ​​ser um loop for, mas também pode ser um código como otherlist.extend(mylist)" -> Isso está incorreto. extend()modifica a lista no local e não retorna um iterável. Tentar fazer um loop over otherlist.extend(mylist)falhará com um TypeErrorporque extend()implicitamente retorna None, e você não poderá fazer o loop over None.
Pedro
4
@pedro Você entendeu mal essa frase. Isso significa que o python executa as duas etapas mencionadas mylist(não ativadas otherlist) durante a execução otherlist.extend(mylist).
hoje
555

Pense desta maneira:

Um iterador é apenas um termo sofisticado para um objeto que possui um next()método. Portanto, uma função de produção acaba sendo algo como isto:

Versão original:

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

for i in some_function():
    print i

Isso é basicamente o que o intérprete Python faz com o código acima:

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

Para obter mais informações sobre o que está acontecendo nos bastidores, o forloop pode ser reescrito para isso:

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

Isso faz mais sentido ou apenas te confunde mais? :)

Devo observar que isso é uma simplificação excessiva para fins ilustrativos. :)

Jason Baker
fonte
1
__getitem__pode ser definido em vez de __iter__. Por exemplo:, class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)Ele imprimirá: 0, 10, 20, ..., 90
jfs 25/10/08
17
Eu tentei este exemplo no Python 3.6 e, se eu criar iterator = some_function(), a variável iteratornão terá mais uma função chamada next(), mas apenas uma __next__()função. Pensei em mencionar.
Peter
Onde a forimplementação do loop que você escreveu chama o __iter__método iterator, a instância instanciada it?
SystematicDisintegration
455

A yieldpalavra-chave é reduzida a dois fatos simples:

  1. Se o compilador detectar a yieldpalavra - chave em qualquer lugar dentro de uma função, essa função não retornará mais por meio da returninstrução Em vez disso , ele retorna imediatamente um objeto "lista pendente" lento chamado gerador
  2. Um gerador é iterável. O que é um iterável ? É qualquer coisa como um listou setou rangeou dict-view, com um built-in de protocolo para visitar cada elemento em uma determinada ordem .

Em poucas palavras: um gerador é uma lista lenta e incrementalmente pendente , e as yieldinstruções permitem que você use a notação de função para programar os valores da lista que o gerador deve citar gradualmente.

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]]

Exemplo

Vamos definir uma função makeRangesemelhante à do Python range. Chamar makeRange(n)RETORNA UM GERADOR:

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>

Para forçar o gerador a retornar imediatamente seus valores pendentes, você pode passá-lo para list()(da mesma forma que você poderia iterável):

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

Comparando o exemplo com "apenas retornando uma lista"

O exemplo acima pode ser pensado como meramente criando uma lista à qual você anexa e retorna:

# 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]

Há uma grande diferença, no entanto; veja a última seção.


Como você pode usar geradores

Um iterável é a última parte da compreensão de uma lista e todos os geradores são iteráveis; portanto, eles são frequentemente usados ​​assim:

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

Para ter uma idéia melhor dos geradores, você pode brincar com o itertoolsmódulo (certifique-se de usá-lo chain.from_iterablee não chainquando necessário). Por exemplo, você pode até usar geradores para implementar listas preguiçosas infinitamente longas como itertools.count(). Você pode implementar por conta própria def enumerate(iterable): zip(count(), iterable)ou alternativamente com a yieldpalavra - chave em um loop while.

Observe: os geradores podem realmente ser usados ​​para muitas outras coisas, como implementar corotinas ou programação não determinística ou outras coisas elegantes. No entanto, o ponto de vista das "listas preguiçosas" que apresento aqui é o uso mais comum que você encontrará.


Por trás das cenas

É assim que o "protocolo de iteração Python" funciona. Ou seja, o que está acontecendo quando você faz list(makeRange(5)). Isso é o que eu descrevi anteriormente como uma "lista lenta e incremental".

>>> 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

A função next()interna apenas chama a .next()função de objetos , que faz parte do "protocolo de iteração" e é encontrada em todos os iteradores. Você pode usar manualmente a next()função (e outras partes do protocolo de iteração) para implementar coisas sofisticadas, geralmente à custa da legibilidade, portanto, evite fazer isso ...


Minutiae

Normalmente, a maioria das pessoas não se importaria com as seguintes distinções e provavelmente desejaria parar de ler aqui.

No Python-speak, iterável é qualquer objeto que "entende o conceito de loop for" como uma lista [1,2,3]e um iterador é uma instância específica do like for loop solicitado [1,2,3].__iter__(). Um gerador é exatamente o mesmo que qualquer iterador, exceto pela forma como foi gravado (com sintaxe da função).

Quando você solicita um iterador de uma lista, ele cria um novo iterador. No entanto, quando você solicita um iterador de um iterador (o que você raramente faria), ele fornece uma cópia de si mesmo.

Assim, no caso improvável de você estar deixando de fazer algo assim ...

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

... lembre-se de que um gerador é um iterador ; isto é, é de uso único. Se você deseja reutilizá-lo, ligue myRange(...)novamente. Se você precisar usar o resultado duas vezes, converta o resultado em uma lista e armazene-o em uma variável x = list(myRange(5)). Aqueles que absolutamente precisam clonar um gerador (por exemplo, que estão fazendo uma metaprogramação assustadoramente hackiana) podem usar, itertools.teese absolutamente necessário, uma vez que a proposta de padrões do iterador copiável do Python PEP foi adiada.

ninjagecko
fonte
378

O que a yieldpalavra - chave faz no Python?

Resumo da resposta / resumo

  • Uma função com yield, quando chamada, retorna um gerador .
  • Os geradores são iteradores porque eles implementam o protocolo do iterador , para que você possa iterar sobre eles.
  • Um gerador também pode receber informações , tornando-o conceitualmente uma rotina .
  • No Python 3, você pode delegar de um gerador para outro nas duas direções com yield from.
  • (O apêndice critica algumas respostas, incluindo a principal, e discute o uso de returnum gerador.)

Geradores:

yieldé legal apenas dentro de uma definição de função, e a inclusão de yieldem uma definição de função faz com que ele retorne um gerador.

A idéia para geradores vem de outros idiomas (consulte a nota de rodapé 1) com diversas implementações. Nos Geradores do Python, a execução do código é congelada no ponto do rendimento. Quando o gerador é chamado (os métodos são discutidos abaixo), a execução é retomada e congela no próximo rendimento.

yieldfornece uma maneira fácil de implementar o protocolo do iterador , definido pelos dois métodos a seguir: __iter__e next(Python 2) ou __next__(Python 3). Ambos os métodos transformam um objeto em um iterador que você pode verificar com a IteratorClasse Base Abstrata do collectionsmódulo.

>>> 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.

O tipo de gerador é um subtipo de iterador:

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

E, se necessário, podemos verificar o seguinte:

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

Uma característica de um Iterator é que, uma vez esgotado , você não pode reutilizá-lo ou redefini-lo:

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

Você precisará fazer outro se quiser usar sua funcionalidade novamente (consulte a nota de rodapé 2):

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

Pode-se produzir dados programaticamente, por exemplo:

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

O gerador simples acima também é equivalente ao abaixo - a partir do Python 3.3 (e não disponível no Python 2), você pode usar yield from:

def func(an_iterable):
    yield from an_iterable

No entanto, yield fromtambém permite delegação a subgeradores, o que será explicado na seção a seguir sobre delegação cooperativa com subcorotinas.

Corotinas:

yield forma uma expressão que permite que os dados sejam enviados ao gerador (consulte a nota de rodapé 3)

Aqui está um exemplo, tome nota da receivedvariável, que apontará para os dados que são enviados ao gerador:

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)

Primeiro, devemos colocar o gerador em fila com a função incorporada next,. Ele vai chamar o apropriado nextou __next__método, dependendo da versão do Python que você está usando:

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

E agora podemos enviar dados para o gerador. ( Enviar Noneé o mesmo que ligarnext .):

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

Delegação Cooperativa à Sub-Corotina com yield from

Agora, lembre-se de que yield fromestá disponível no Python 3. Isso nos permite delegar corotinas a uma subcorotina:

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()

E agora podemos delegar funcionalidade a um subgerador e ele pode ser usado por um gerador, como acima:

>>> 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

Você pode ler mais sobre a semântica precisa de yield fromno PEP 380.

Outros métodos: fechar e atirar

O closemétodo aumenta GeneratorExitno ponto em que a execução da função foi congelada. Isso também será chamado __del__para que você possa colocar qualquer código de limpeza onde manipular GeneratorExit:

>>> my_account.close()

Você também pode lançar uma exceção que pode ser manipulada no gerador ou propagada de volta para o usuário:

>>> 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

Conclusão

Acredito ter coberto todos os aspectos da seguinte pergunta:

O que a yieldpalavra - chave faz no Python?

Acontece que isso yieldfaz muito. Tenho certeza de que poderia adicionar exemplos ainda mais detalhados a isso. Se você quiser mais ou tiver alguma crítica construtiva, deixe-me saber comentando abaixo.


Apêndice:

Crítica da resposta principal / aceita **

  • Está confuso o que torna um iterável , apenas usando uma lista como exemplo. Veja minhas referências acima, mas em resumo: um iterável tem um __iter__método retornando um iterador . Um iterador fornece um método .next(Python 2 ou .__next__(Python 3), que é implicitamente chamado por forloops até que seja gerado StopIteratione, assim que for, continuará a fazê-lo.
  • Em seguida, ele usa uma expressão de gerador para descrever o que é um gerador. Como um gerador é simplesmente uma maneira conveniente de criar um iterador , ele apenas confunde o assunto, e ainda não chegamos a esse yieldponto.
  • Em Controlando uma exaustão de gerador, ele chama o .nextmétodo, quando, em vez disso, ele deve usar a função embutida next,. Seria uma camada de indireção apropriada, porque seu código não funciona no Python 3.
  • Itertools? Isso não foi relevante para o que yieldfaz.
  • Nenhuma discussão sobre os métodos que yieldacompanham a nova funcionalidade yield fromdo Python 3. A resposta superior / aceita é uma resposta muito incompleta.

Crítica da resposta, sugerindo yielduma expressão ou compreensão geradora.

Atualmente, a gramática permite qualquer expressão na compreensão de uma lista.

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

Como rendimento é uma expressão, alguns consideram interessante usá-lo em compreensões ou expressões geradoras - apesar de não citar nenhum caso de uso particularmente bom.

Os desenvolvedores principais do CPython estão discutindo a depreciação de sua permissão . Aqui está uma postagem relevante da lista de discussão:

Em 30 de janeiro de 2017 às 19:05, Brett Cannon escreveu:

Em domingo, 29 de janeiro de 2017 às 16:39 Craig Rodrigues escreveu:

Eu estou bem com qualquer uma das abordagens. Deixar as coisas do jeito que estão no Python 3 não é bom, IMHO.

Meu voto é que seja um SyntaxError, já que você não está obtendo o que espera da sintaxe.

Concordo que é um local sensato para terminarmos, pois qualquer código que se baseia no comportamento atual é realmente muito inteligente para ser sustentável.

Em termos de chegar lá, provavelmente desejaremos:

  • SyntaxWarning ou DeprecationWarning na versão 3.7
  • Aviso de Py3k no 2.7.x
  • SyntaxError em 3.8

Saúde, Nick.

- Nick Coghlan | ncoghlan em gmail.com | Brisbane, Austrália

Além disso, há um problema pendente (10544) que parece apontar na direção de que isso nunca seja uma boa ideia (PyPy, uma implementação do Python escrita em Python, já está levantando avisos de sintaxe.)

Resumindo, até que os desenvolvedores do CPython nos digam o contrário: não coloque yielduma expressão ou compreensão geradora.

A returndeclaração em um gerador

No Python 2 :

Em uma função de gerador, a returninstrução não tem permissão para incluir um expression_list. Nesse contexto, um vazio returnindica que o gerador está pronto e fará com StopIterationque seja gerado.

An expression_listé basicamente qualquer número de expressões separadas por vírgulas - essencialmente, no Python 2, você pode parar o gerador com return, mas não pode retornar um valor.

No Python 3 :

Em uma função de gerador, a returninstrução indica que o gerador está pronto e fará com StopIterationque seja gerado. O valor retornado (se houver) é usado como argumento para construir StopIteratione se torna o StopIteration.valueatributo.

Notas de rodapé

  1. As linguagens CLU, Sather e Icon foram referenciadas na proposta para introduzir o conceito de geradores no Python. A ideia geral é que uma função possa manter o estado interno e gerar pontos de dados intermediários sob demanda do usuário. Isso prometeu ter desempenho superior a outras abordagens, incluindo o Python Threading , que nem está disponível em alguns sistemas.

  2. Isso significa, por exemplo, que os xrangeobjetos ( rangeno Python 3) não são Iterators, mesmo que sejam iteráveis, porque podem ser reutilizados. Como listas, seus __iter__métodos retornam objetos iteradores.

  3. yieldfoi originalmente introduzido como uma declaração, o que significa que só poderia aparecer no início de uma linha em um bloco de código. Agora yieldcria uma expressão de rendimento. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Esta alteração foi proposta para permitir que um usuário envie dados para o gerador da mesma forma que se poderia receber. Para enviar dados, é preciso poder atribuí-los a alguma coisa e, para isso, uma declaração simplesmente não funcionará.

Aaron Hall
fonte
328

yieldé exatamente como return- retorna o que você deseja (como um gerador). A diferença é que na próxima vez em que você ligar para o gerador, a execução começará da última chamada para a yieldinstrução. Diferentemente do retorno, o quadro da pilha não é limpo quando ocorre um rendimento, no entanto, o controle é transferido de volta para o chamador, portanto seu estado será retomado na próxima vez que a função for chamada.

No caso do seu código, a função get_child_candidatesestá agindo como um iterador, de modo que, quando você estende sua lista, ele adiciona um elemento de cada vez à nova lista.

list.extendchama um iterador até que esteja esgotado. No caso do exemplo de código que você postou, seria muito mais claro retornar uma tupla e anexá-la à lista.

Douglas Mayle
fonte
107
Isso está próximo, mas não está correto. Toda vez que você chama uma função com uma declaração de rendimento, ela retorna um novo objeto gerador. Somente quando você chama o método .next () desse gerador, a execução é retomada após o último rendimento.
kurosch 24/10/08
239

Há uma coisa extra a ser mencionada: uma função que produz realmente não precisa terminar. Eu escrevi código assim:

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

Então eu posso usá-lo em outro código como este:

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

Isso realmente ajuda a simplificar alguns problemas e facilita algumas coisas.

Claudiu
fonte
233

Para aqueles que preferem um exemplo de trabalho mínimo, medite nesta sessão interativa do 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
Daniel
fonte
209

TL; DR

Em vez disso:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

faça isso:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Sempre que você se encontrar criando uma lista do zero, yieldcada peça é substituída.

Este foi o meu primeiro momento "aha" com rendimento.


yieldé uma maneira açucarada de dizer

construir uma série de coisas

Mesmo comportamento:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Comportamento diferente:

O rendimento é de passagem única : você pode iterar apenas uma vez. Quando uma função tem um rendimento nela, chamamos de função geradora . E um iterador é o que ele retorna. Esses termos são reveladores. Perdemos a conveniência de um contêiner, mas obtemos o poder de uma série calculada conforme necessário e arbitrariamente longa.

O rendimento é preguiçoso , adia a computação. Uma função com um rendimento na verdade não é executada quando você a chama. Ele retorna um objeto iterador que lembra de onde parou. Cada vez que você chama next()o iterador (isso acontece em um loop for), a polegada avança para o próximo rendimento. returngera StopIteration e encerra a série (esse é o fim natural de um loop for).

O rendimento é versátil . Os dados não precisam ser armazenados juntos, eles podem ser disponibilizados um de cada vez. Pode ser infinito.

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Se você precisar de vários passes e a série não for muito longa, basta ligar list()para ele:

>>> list(square_yield(4))
[0, 1, 4, 9]

Escolha brilhante da palavra yieldporque ambos os significados se aplicam:

rendimento - produz ou fornece (como na agricultura)

... forneça os próximos dados da série.

rendimento - ceder ou renunciar (como no poder político)

... renuncie à execução da CPU até o iterador avançar.

Bob Stein
fonte
194

O rendimento fornece um gerador.

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

Como você pode ver, no primeiro caso, foomantém a lista inteira na memória de uma só vez. Não é grande coisa para uma lista com 5 elementos, mas e se você quiser uma lista de 5 milhões? Isso não apenas é um grande consumidor de memória, mas também custa muito tempo para ser construído no momento em que a função é chamada.

No segundo caso, barapenas fornece um gerador. Um gerador é iterável - o que significa que você pode usá-lo em um forloop, etc., mas cada valor pode ser acessado apenas uma vez. Todos os valores também não são armazenados na memória ao mesmo tempo; o objeto gerador "lembra" onde estava o loop na última vez em que você o chamou - dessa forma, se você estiver usando um iterável para (digamos) contar até 50 bilhões, não precisará contar até 50 bilhões de uma vez e armazene os 50 bilhões de números para contar.

Novamente, este é um exemplo bem elaborado, você provavelmente usaria ferramentas se realmente quisesse contar até 50 bilhões. :)

Este é o caso de uso mais simples de geradores. Como você disse, ele pode ser usado para escrever permutações eficientes, usando yield para fazer as coisas subirem pela pilha de chamadas em vez de usar algum tipo de variável de pilha. Os geradores também podem ser usados ​​para travessias especializadas de árvores e todo tipo de outras coisas.

RBansal
fonte
Apenas uma observação - no Python 3, rangetambém retorna um gerador em vez de uma lista; portanto, você também vê uma idéia semelhante, exceto que __repr__/ __str__é substituída para mostrar um resultado melhor, nesse caso range(1, 10, 2).
caso.
189

Está retornando um gerador. Eu não estou particularmente familiarizado com o Python, mas acredito que é o mesmo tipo de bloco iterador do C # se você estiver familiarizado com eles.

A idéia principal é que o compilador / intérprete / o que quer que faça algum truque para que, no que diz respeito ao chamador, ele possa continuar chamando next () e ele continuará retornando valores - como se o método do gerador estivesse em pausa . Agora, obviamente, você não pode realmente "pausar" um método, de modo que o compilador constrói uma máquina de estado para você lembrar onde está atualmente e como são as variáveis ​​locais, etc. Isso é muito mais fácil do que escrever um iterador.

Jon Skeet
fonte
167

Há um tipo de resposta que ainda não me foi dado, dentre as muitas ótimas respostas que descrevem como usar geradores. Aqui está a resposta da teoria da linguagem de programação:

A yielddeclaração em Python retorna um gerador. Um gerador em Python é uma função que retorna continuações (e especificamente um tipo de corotina, mas continuações representam o mecanismo mais geral para entender o que está acontecendo).

As continuações na teoria das linguagens de programação são um tipo de computação muito mais fundamental, mas não são frequentemente usadas, porque são extremamente difíceis de raciocinar e também muito difíceis de implementar. Mas a idéia do que é uma continuação é direta: é o estado de uma computação que ainda não terminou. Nesse estado, os valores atuais das variáveis, as operações que ainda precisam ser executadas etc. são salvas. Então, em algum momento posterior do programa, a continuação poderá ser chamada, de modo que as variáveis ​​do programa sejam redefinidas para esse estado e as operações salvas sejam executadas.

As continuações, nesta forma mais geral, podem ser implementadas de duas maneiras. No call/cccaminho, a pilha do programa é literalmente salva e, quando a continuação é chamada, a pilha é restaurada.

No estilo de passagem de continuação (CPS), continuações são apenas funções normais (somente em idiomas onde as funções são de primeira classe) que o programador gerencia e explicitamente gerencia e repassa para as sub-rotinas. Nesse estilo, o estado do programa é representado por fechamentos (e as variáveis ​​que são codificadas neles), em vez de variáveis ​​que residem em algum lugar da pilha. As funções que gerenciam o fluxo de controle aceitam a continuação como argumentos (em algumas variações do CPS, as funções podem aceitar várias continuações) e manipulam o fluxo de controle chamando-as simplesmente chamando-as e retornando posteriormente. Um exemplo muito simples de estilo de passagem de continuação é o seguinte:

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)

Neste exemplo (muito simplista), o programador salva a operação de gravar o arquivo em uma continuação (que pode ser uma operação muito complexa com muitos detalhes a serem gravados) e depois passa essa continuação (por exemplo, fechamento de classe) para outro operador que faz mais processamento e depois o chama, se necessário. (Eu uso muito esse padrão de design na programação da GUI real, porque me salva linhas de código ou, mais importante, para gerenciar o fluxo de controle após o acionamento dos eventos da GUI.)

O restante deste post, sem perda de generalidade, conceituará continuações como CPS, porque é muito mais fácil de entender e ler.


Agora vamos falar sobre geradores em Python. Geradores são um subtipo específico de continuação. Enquanto as continuações geralmente são capazes de salvar o estado de uma computação (ou seja, a pilha de chamadas do programa), os geradores só podem salvar o estado da iteração em um iterador . Embora essa definição seja um pouco enganadora para certos casos de uso de geradores. Por exemplo:

def f():
  while True:
    yield 4

É claramente uma iterável razoável, cujo comportamento é bem definido - sempre que o gerador itera, ele retorna 4 (e faz isso para sempre). Mas provavelmente não é o tipo de iterável prototípico que vem à mente quando se pensa em iteradores (ie for x in collection: do_something(x)). Este exemplo ilustra o poder dos geradores: se algo é um iterador, um gerador pode salvar o estado de sua iteração.

Para reiterar: As continuações podem salvar o estado da pilha de um programa e os geradores podem salvar o estado da iteração. Isso significa que as continuações são muito mais poderosas que os geradores, mas também que os geradores são muito, muito mais fáceis. Eles são mais fáceis de implementar pelo designer de linguagem e mais fáceis de usar pelo programador (se você tiver algum tempo para gravar, tente ler e entender esta página sobre continuações e chamar / cc ).

Mas você pode facilmente implementar (e conceituar) geradores como um caso simples e específico de estilo de passagem de continuação:

Sempre que yieldé chamado, ele informa a função para retornar uma continuação. Quando a função é chamada novamente, ela começa de onde parou. Portanto, no pseudo-pseudocódigo (ou seja, não no pseudocódigo, mas não no código), o nextmétodo do gerador é basicamente o seguinte:

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

onde a yieldpalavra-chave é realmente açúcar sintático para a função de gerador real, basicamente algo como:

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

Lembre-se de que este é apenas um pseudocódigo e a implementação real de geradores em Python é mais complexa. Mas, como um exercício para entender o que está acontecendo, tente usar o estilo de passagem de continuação para implementar objetos geradores sem o uso da yieldpalavra - chave.

aestrivex
fonte
152

Aqui está um exemplo em linguagem simples. Fornecerei uma correspondência entre conceitos humanos de alto nível e conceitos Python de baixo nível.

Quero operar em uma sequência de números, mas não quero me preocupar com a criação dessa sequência, quero apenas me concentrar na operação que quero fazer. Então, eu faço o seguinte:

  • Ligo para você e digo que quero uma sequência de números que seja produzida de uma maneira específica e que você saiba qual é o algoritmo.
    Este passo corresponde à entrada da deffunção do gerador, ou seja, a função que contém a yield.
  • Algum tempo depois, digo: "OK, prepare-se para me contar a sequência dos números".
    Esta etapa corresponde à chamada da função de gerador que retorna um objeto gerador. Observe que você ainda não me disse números; você apenas pega seu papel e lápis.
  • Eu pergunto: "diga-me o próximo número" e você me diga o primeiro número; depois disso, você espera que eu peça o próximo número. É seu trabalho lembrar onde você estava, quais números você já disse e qual é o próximo número. Eu não ligo para os detalhes.
    Esta etapa corresponde à chamada .next()no objeto gerador.
  • … Repita o passo anterior, até…
  • eventualmente, você pode chegar ao fim. Você não me conta um número; você apenas grita: "segure seus cavalos! Eu terminei! Sem mais números!"
    Esta etapa corresponde ao objeto gerador que encerra seu trabalho e gera uma StopIterationexceção. A função gerador não precisa gerar a exceção. É gerado automaticamente quando a função termina ou emite a return.

É isso que um gerador faz (uma função que contém a yield); começa a executar, pausa sempre que faz a yielde, quando solicitado por um .next()valor, continua do ponto em que foi o último. Ele se encaixa perfeitamente no design com o protocolo iterador do Python, que descreve como solicitar valores seqüencialmente.

O usuário mais famoso do protocolo iterador é o forcomando no Python. Portanto, sempre que você faz um:

for item in sequence:

não importa se sequenceé uma lista, uma string, um dicionário ou um objeto gerador como descrito acima; o resultado é o mesmo: você lê os itens de uma sequência, um por um.

Observe que definserir uma função que contém uma yieldpalavra-chave não é a única maneira de criar um gerador; é apenas a maneira mais fácil de criar uma.

Para obter informações mais precisas, leia sobre os tipos de iteradores , a declaração de rendimento e os geradores na documentação do Python.

tzot
fonte
130

Embora muitas respostas mostrem por que você usaria a yieldpara criar um gerador, há mais usos para yield. É muito fácil fazer uma rotina, o que permite a passagem de informações entre dois blocos de código. Não repetirei nenhum dos bons exemplos que já foram dados sobre o uso yieldpara criar um gerador.

Para ajudar a entender o que a yieldfaz no código a seguir, você pode usar o dedo para rastrear o ciclo através de qualquer código que possua a yield. Toda vez que seu dedo toca no yield, você precisa aguardar a inserção de um nextou um send. Quando a nexté chamado, você rastreia o código até pressionar o yield... o código à direita do yieldé avaliado e retornado ao chamador ... e então você espera. Quando nexté chamado novamente, você executa outro loop pelo código. No entanto, você notará que em uma rotina yieldtambém pode ser usado com um send… que enviará um valor do chamador para a função de retorno. Se a sendé dado, entãoyieldrecebe o valor enviado e cospe no lado esquerdo ... então o rastreamento através do código progride até que você pressione yieldnovamente (retornando o valor no final, como se nextfosse chamado).

Por exemplo:

>>> 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()
Mike McKerns
fonte
Fofa! Um trampolim (no sentido Lisp). Muitas vezes não se vê aqueles!
00prometheus
129

Há outro yielduso e significado (desde o Python 3.3):

yield from <expr>

Do PEP 380 - Sintaxe para delegar a um subgerador :

Uma sintaxe é proposta para um gerador delegar parte de suas operações para outro gerador. Isso permite que uma seção de código contendo 'yield' seja fatorada e colocada em outro gerador. Além disso, o subgerador pode retornar com um valor, e o valor é disponibilizado ao gerador de delegação.

A nova sintaxe também abre algumas oportunidades de otimização quando um gerador retorna os valores produzidos por outro.

Além disso, isso apresentará (desde o Python 3.5):

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

para evitar que as corotinas sejam confundidas com um gerador comum (hoje yieldé usado em ambos).

Sławomir Lenart
fonte
117

Todas as ótimas respostas, porém um pouco difíceis para iniciantes.

Presumo que você tenha aprendido a returnafirmação.

Como uma analogia, returne yieldsão gêmeos. returnsignifica 'retornar e parar', enquanto 'rendimento' significa 'retornar, mas continuar'

  1. Tente obter um num_list com return.
def num_list(n):
    for i in range(n):
        return i

Executá-lo:

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

Veja, você obtém apenas um único número em vez de uma lista deles. returnnunca permite que você prevaleça feliz, apenas implementa uma vez e sai.

  1. Lá vem yield

Substitua returnpor 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]

Agora, você ganha para obter todos os números.

Comparando com o returnque é executado uma vez e para, os yieldtempos de execução planejados. Você pode interpretar returncomo return one of them, e yieldcomo return all of them. Isso é chamado iterable.

  1. Mais uma etapa, podemos reescrever a yielddeclaração comreturn
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]

É o núcleo sobre yield.

A diferença entre as returnsaídas de uma lista e a yieldsaída do objeto é:

Você sempre obterá [0, 1, 2] de um objeto de lista, mas somente poderá recuperá-los da ' yieldsaída do objeto ' uma vez. Portanto, ele possui um novo generatorobjeto de nome, conforme exibido em Out[11]: <generator object num_list at 0x10327c990>.

Em conclusão, como uma metáfora para grok-lo:

  • returne yieldsão gêmeos
  • liste generatorsão gêmeos
Cálculo
fonte
Isso é compreensível, mas uma grande diferença é que você pode ter vários rendimentos em uma função / método. A analogia se quebra totalmente nesse ponto. O rendimento lembra seu lugar em uma função; portanto, da próxima vez que você ligar para next (), sua função continuará para a próxima yield. Acho que isso é importante e deve ser expresso.
Mike S
104

Aqui estão alguns exemplos de Python de como realmente implementar geradores como se o Python não fornecesse açúcar sintático para eles:

Como um gerador 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))

Usando fechamentos lexicais em vez de geradores

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)

Usando fechamentos de objetos em vez de geradores (porque 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)
Dustin Getz
fonte
97

Eu publicaria "leia a página 19 do 'Python: Essential Reference' de Beazley para uma descrição rápida dos geradores", mas muitos outros já publicaram boas descrições.

Além disso, observe que yieldpode ser usado em corotinas como o duplo uso em funções de gerador. Embora não seja o mesmo uso que seu snippet de código, (yield)pode ser usado como uma expressão em uma função. Quando um chamador envia um valor para o método usando o send()método, a corotina será executada até a próxima(yield) instrução seja encontrada.

Geradores e corotinas são uma maneira legal de configurar aplicativos do tipo fluxo de dados. Eu pensei que valeria a pena conhecer o outro uso da yielddeclaração em funções.

johnzachary
fonte
97

Do ponto de vista da programação, os iteradores são implementados como thunks .

Para implementar iteradores, geradores e conjuntos de encadeamentos para execução simultânea etc. como thunks (também chamados de funções anônimas), usa-se mensagens enviadas para um objeto de fechamento, que possui um despachante, e o despachante responde a "mensagens".

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

" next " é uma mensagem enviada para um encerramento, criada pelo " iter chamada " ".

Existem várias maneiras de implementar esse cálculo. Eu usei mutação, mas é fácil fazê-lo sem mutação, retornando o valor atual e o próximo usuário.

Aqui está uma demonstração que usa a estrutura do R6RS, mas a semântica é absolutamente idêntica à do Python. É o mesmo modelo de computação, e apenas uma mudança na sintaxe é necessária para reescrevê-la no 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
->
alinsoar
fonte
84

Aqui está um exemplo simples:

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)

Resultado:

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

Não sou desenvolvedor de Python, mas parece-me yield mantém a posição do fluxo do programa e o próximo loop começa na posição "yield". Parece que está esperando nessa posição, e pouco antes disso, retornando um valor para fora, e da próxima vez continua a funcionar.

Parece ser uma habilidade interessante e interessante: D

Engin OZTURK
fonte
Você está certo. Mas qual é o efeito no fluxo que é ver o comportamento do "rendimento"? Eu posso mudar o algoritmo em nome da matemática. Ajudará a obter uma avaliação diferente do "rendimento"?
Engin OZTURK
68

Aqui está uma imagem mental do que yieldfaz.

Eu gosto de pensar em um thread como tendo uma pilha (mesmo quando não é implementada dessa maneira).

Quando uma função normal é chamada, ela coloca suas variáveis ​​locais na pilha, faz alguns cálculos, depois limpa a pilha e retorna. Os valores de suas variáveis ​​locais nunca são vistos novamente.

Com uma yieldfunção, quando seu código começa a ser executado (ou seja, depois que a função é chamada, retornando um objeto gerador, cujo next()método é então invocado), ele também coloca suas variáveis ​​locais na pilha e calcula por um tempo. Porém, quando atinge a yieldinstrução, antes de limpar sua parte da pilha e retornar, tira um instantâneo de suas variáveis ​​locais e as armazena no objeto gerador. Ele também anota o local em que está atualmente em seu código (ou seja, a yieldinstrução específica ).

Portanto, é uma espécie de função congelada na qual o gerador está pendurado.

Quando next()é chamado posteriormente, ele recupera os pertences da função na pilha e a anima novamente. A função continua a computar de onde parou, alheia ao fato de ter passado apenas uma eternidade em armazenamento refrigerado.

Compare os seguintes exemplos:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Quando chamamos a segunda função, ela se comporta de maneira muito diferente da primeira. A yielddeclaração pode estar inacessível, mas se estiver presente em qualquer lugar, muda a natureza do que estamos lidando.

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

A chamada yielderFunction()não executa seu código, mas gera um gerador a partir do código. (Talvez seja uma boa idéia nomear essas coisas com o yielderprefixo de legibilidade.)

>>> 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']

Os campos gi_codee gi_framesão onde o estado congelado é armazenado. Explorando-os dir(..), podemos confirmar que nosso modelo mental acima é credível.

Evgeni Sergeev
fonte
59

Como toda resposta sugere, yieldé usado para criar um gerador de sequência. É usado para gerar alguma sequência dinamicamente. Por exemplo, ao ler um arquivo linha por linha em uma rede, você pode usar a yieldfunção da seguinte maneira:

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

Você pode usá-lo em seu código da seguinte maneira:

for line in getNextLines():
    doSomeThing(line)

Pega de transferência de controle de execução

O controle de execução será transferido de getNextLines () para o forloop quando o rendimento for executado. Assim, toda vez que getNextLines () é chamado, a execução começa a partir do ponto em que foi pausada na última vez.

Assim, resumindo, uma função com o seguinte código

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

for i in simpleYield():
    print i

irá imprimir

"first time"
"second time"
"third time"
"Now some useful value 12"
Mangu Singh Rajpurohit
fonte
59

Um exemplo fácil de entender o que é: yield

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


for i in f123():
    print (i)

A saída é:

1 2 1 2 1 2 1 2
Gavriel Cohen
fonte
5
você tem certeza sobre essa saída? não seria impresso apenas em uma única linha se você executasse essa declaração de impressão usando print(i, end=' ')? Caso contrário, acredito que o comportamento padrão colocaria cada número em uma nova linha
user9074332
@ user9074332, Você está certo, mas está escrito em uma linha para facilitar a compreensão
Gavriel Cohen
57

(Minha resposta abaixo fala apenas da perspectiva de usar o gerador Python, não da implementação subjacente do mecanismo do gerador , que envolve alguns truques de manipulação de pilha e pilha.)

Quando yieldé usado em vez de returnem uma função python, essa função é transformada em algo especial chamado generator function. Essa função retornará um objeto do generatortipo A yieldpalavra-chave é um sinalizador para notificar o compilador python para tratar essa função especialmente. As funções normais serão encerradas assim que algum valor for retornado. Mas com a ajuda do compilador, a função do gerador pode ser considerada como recuperável. Ou seja, o contexto de execução será restaurado e a execução continuará na última execução. Até você chamar explicitamente return, o que gerará uma StopIterationexceção (que também faz parte do protocolo iterador) ou chegará ao final da função. Eu encontrei um monte de referências sobre generator, mas este umdo functional programming perspectiveé o mais digerível.

(Agora, quero falar sobre a lógica por trás generatore com iteratorbase no meu próprio entendimento. Espero que isso possa ajudá-lo a entender a motivação essencial do iterador e gerador. Esse conceito aparece em outros idiomas e também em C #.)

Pelo que entendi, quando queremos processar um monte de dados, geralmente armazenamos os dados em algum lugar e depois os processamos um a um. Mas essa abordagem ingênua é problemática. Se o volume de dados for grande, é caro armazená-los como um todo antecipadamente. Então, em vez de armazenar o datapróprio diretamente, por que não armazenar metadataindiretamente, iethe logic how the data is computed .

Existem 2 abordagens para agrupar esses metadados.

  1. A abordagem OO, envolvemos os metadados as a class. É o chamado iteratorque implementa o protocolo iterador (ou seja __next__(), os __iter__()métodos). Esse também é o padrão de design do iterador comumente visto .
  2. A abordagem funcional, envolvemos os metadados as a function. Este é o chamado generator function. Mas, sob o capô, o iterador generator objectimóvel retornado IS-Aporque também implementa o protocolo do iterador.

De qualquer maneira, um iterador é criado, ou seja, algum objeto que pode fornecer os dados que você deseja. A abordagem OO pode ser um pouco complexa. Enfim, qual deles usar depende de você.

smwikipedia
fonte
54

Em resumo, a yieldinstrução transforma sua função em uma fábrica que produz um objeto especial chamado a generatorque envolve o corpo da sua função original. Quando generatoriterado, ele executa sua função até chegar ao próximo yield, suspende a execução e avalia o valor passado para yield. Ele repete esse processo em cada iteração até que o caminho da execução saia da função. Por exemplo,

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

for i in simple_generator():
    print i

simplesmente gera

one
two
three

A potência vem do uso do gerador com um loop que calcula uma sequência, o gerador executa o loop cada vez para 'produzir' o próximo resultado do cálculo, dessa forma calcula uma lista em tempo real, sendo o benefício a memória salvo para cálculos especialmente grandes

Digamos que você queira criar uma rangefunção própria que produza um intervalo iterável de números, você pode fazê-lo assim,

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

e use assim;

for i in myRangeNaive(10):
    print i

Mas isso é ineficiente porque

  • Você cria uma matriz que você usa apenas uma vez (isso desperdiça memória)
  • Na verdade, esse código faz um loop sobre esse array duas vezes! :(

Felizmente Guido e sua equipe foram generosos o suficiente para desenvolver geradores para que pudéssemos fazer isso;

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

for i in myRangeSmart(10):
    print i

Agora, a cada iteração, uma função no gerador chamado next()executa a função até atingir uma instrução 'yield' na qual para e 'produz' o valor ou atinge o final da função. Nesse caso, na primeira chamada, next()executa até a declaração de rendimento e produz 'n'; na próxima chamada, ela executará a declaração de incremento, retornará ao 'while', avaliará e, se for verdadeiro, irá parar e yield 'n' novamente, continuará assim até que a condição while retorne falsa e o gerador salte para o final da função.

redbandit
fonte
53

Rendimento é um objeto

A returnem uma função retornará um único valor.

Se você deseja que uma função retorne um conjunto enorme de valores , use yield.

Mais importante, yieldé uma barreira .

como barreira no idioma CUDA, ele não transferirá o controle até que seja concluído.

Ou seja, ele executará o código em sua função desde o início até que seja atingido yield. Então, ele retornará o primeiro valor do loop.

Em seguida, todas as outras chamadas executam o loop que você escreveu na função mais uma vez, retornando o próximo valor até que não haja nenhum valor a ser retornado.

Kaleem Ullah
fonte
52

Muitas pessoas usam returne não yield, mas em alguns casos yieldpodem ser mais eficientes e fáceis de trabalhar.

Aqui está um exemplo que yieldé definitivamente melhor para:

return (em função)

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

rendimento (em função)

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.

Chamando funções

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)

Ambas as funções fazem a mesma coisa, mas yieldusam três linhas em vez de cinco e tem uma variável a menos para se preocupar.

Este é o resultado do código:

Resultado

Como você pode ver, ambas as funções fazem a mesma coisa. A única diferença é return_dates()fornecer uma lista e yield_dates()gerar um gerador.

Um exemplo da vida real seria algo como ler um arquivo linha por linha ou se você apenas deseja criar um gerador.

Tom Fuller
fonte
43

yieldé como um elemento de retorno para uma função. A diferença é que o yieldelemento transforma uma função em um gerador. Um gerador se comporta como uma função até que algo seja 'produzido'. O gerador para até a próxima chamada e continua exatamente do mesmo ponto em que foi iniciado. Você pode obter uma sequência de todos os valores 'produzidos' em um, chamando list(generator()).

Will Dereham
fonte
41

A yieldpalavra-chave simplesmente coleta os resultados retornados. Pense yieldcomoreturn +=

Bahtiyar Özdere
fonte
36

Aqui está uma yieldabordagem simples , para calcular a série de fibonacci, explicada:

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

Quando você digita isso no seu REPL e tenta chamá-lo, você obtém um resultado intrigante:

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

Isso ocorre porque a presença de yieldsinalizou para Python que você deseja criar um gerador , ou seja, um objeto que gera valores sob demanda.

Então, como você gera esses valores? Isso pode ser feito diretamente usando a função interna nextou, indiretamente, alimentando-a com uma construção que consome valores.

Usando a next()função interna, você invoca diretamente .next/ __next__, forçando o gerador a produzir um valor:

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

Indiretamente, se você fornecer fibum forloop, um listinicializador, um tupleinicializador ou qualquer outra coisa que espere um objeto que gere / produz valores, você "consumirá" o gerador até que nenhum outro valor possa ser produzido por ele (e ele retornará) :

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

Da mesma forma, com um tupleinicializador:

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

Um gerador difere de uma função no sentido de ser preguiçoso. Isso é realizado mantendo o estado local e permitindo que você retome sempre que precisar.

Quando você invoca fibpela primeira vez , chamando-o:

f = fib()

Python compila a função, encontra a yieldpalavra - chave e simplesmente retorna um objeto gerador de volta para você. Parece que não é muito útil.

Quando você solicita que ele gere o primeiro valor, direta ou indiretamente, ele executa todas as instruções que encontra, até encontrar um yield, e retorna o valor que você forneceu yielde pausa. Para um exemplo que melhor demonstra isso, vamos usar algumas printchamadas (substitua por print "text"if no 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.")

Agora, entre no REPL:

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

você tem um objeto gerador agora aguardando um comando para gerar um valor. Use nexte veja o que é impresso:

>>> 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!'

Os resultados não citados são impressos. O resultado citado é o que é retornado yield. Ligue nextnovamente agora:

>>> 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!'

O gerador lembra que foi interrompido yield valuee continua a partir daí. A próxima mensagem é impressa e a busca pela yieldinstrução em pausa é executada novamente (devido ao whileloop).

Dimitris Fasarakis Hilliard
fonte