geradores - yield python




O que a palavra-chave "yield" faz? (20)

Qual é o uso da palavra-chave yield em Python? O que isso 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 subseqüentes serão interrompidas?

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


O que a palavra-chave yield faz no Python?

Resumo da Resposta / Resumo

  • Uma função com yield , quando chamada, retorna um Generator .
  • Os geradores são iteradores porque implementam o protocolo do iterador , para que você possa iterá-los.
  • Um gerador também pode receber informações , tornando-se conceitualmente uma co - rotina .
  • No Python 3, você pode delegar de um gerador para outro em ambas as direções com yield from .
  • (Apêndice critica algumas respostas, incluindo a primeira, e discute o uso de return em um gerador.)

Geradores:

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

A ideia dos geradores vem de outras linguagens (veja a nota de rodapé 1) com implementações variadas. Nos Geradores do Python, a execução do código é frozen no ponto do rendimento. Quando o gerador é chamado (os métodos são discutidos abaixo), a execução é retomada e, em seguida, congela no próximo rendimento.

yield fornece 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 tornam um objeto um iterador que você pode digitar com a classe básica abstrata do Iterator do módulo de collections .

>>> 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 como este:

>>> 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ê terá que fazer outro se quiser usar sua funcionalidade novamente (consulte a nota de rodapé 2):

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

Um pode gerar dados por meio de programação, 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 o yield from :

def func(an_iterable):
    yield from an_iterable

No entanto, o yield from também permite a delegação aos subgeradores, o que será explicado na seção seguinte sobre delegação cooperativa com sub-corrotinas.

Coroutines:

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

Aqui está um exemplo, tome nota da variável received , que apontará para os dados que são enviados para o 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 enfileirar o gerador com a função interna, a next . Ele chamará o método next ou __next__ apropriado, 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 chamar de next .):

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

Delegação Cooperativa para Sub-Coroutine com yield from

Agora, lembre-se de que yield from está disponível no Python 3. Isso nos permite delegar corrotinas a uma subcorestina:

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 sub-gerador e 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 from no PEP 380.

Outros métodos: fechar e jogar

O método close aumenta o GeneratorExit no ponto em que a execução da função foi congelada. Isso também será chamado por __del__ para que você possa colocar qualquer código de limpeza onde você manipule o 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 palavra-chave yield faz no Python?

Acontece que o yield faz muito. Tenho certeza que poderia adicionar exemplos ainda mais completos para isso. Se você quer mais ou tem algumas críticas construtivas, me avise comentando abaixo.

Apêndice:

Crítica da Resposta Top / Aceita **

  • É confuso sobre 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 método __iter__ retornando um iterador . Um iterador fornece um método .__next__ (Python 2 ou .__next__ (Python 3), que é implicitamente chamado por for loops até que ele aumente StopIteration e, quando o fizer, 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 à parte de yield .
  • Em Controlar o esgotamento de um gerador ele chama o método .next , quando em vez disso ele deve usar a função interna, a next . Seria uma camada apropriada de indireção, porque seu código não funciona no Python 3.
  • Itertools? Isso não foi relevante para o yield .
  • Nenhuma discussão sobre os métodos que yield fornece junto com o novo yield from funcionalidade no Python 3. A resposta top / aceita é uma resposta muito incompleta.

Crítica de resposta sugerindo yield em uma expressão ou compreensão geradora.

A gramática atualmente permite qualquer expressão em uma compreensão de 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 o rendimento é uma expressão, alguns os consideram interessantes para usá-lo em expressões ou expressões geradoras - apesar de não citar nenhum caso de uso particularmente bom.

Os desenvolvedores do núcleo CPython estão discutindo depreciar 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:

Dom Dom, 29 Jan 2017 at 16:39 Craig Rodrigues escreveu:

Eu estou bem com qualquer abordagem. Deixar as coisas como estão no Python 3 não é bom, IMHO.

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

Eu concordo que é um lugar sensato para acabarmos, já que qualquer código que se baseie no comportamento atual é realmente inteligente demais para ser passível de manutenção.

Em termos de chegar lá, provavelmente queremos:

  • SintaxeWarning ou DeprecationWarning in 3.7
  • Alerta Py3k em 2.7.x
  • SyntaxError in 3.8

Felicidades, Nick.

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

Além disso, há uma questão pendente (10544) que parece estar apontando na direção de nunca ser uma boa ideia (PyPy, uma implementação em Python escrita em Python, já está levantando avisos de sintaxe).

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

A returnafirmação em um gerador

No Python 2 :

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

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 você não pode retornar um valor.

Em Python 3 :

Em uma função geradora, a returndeclaração indica que o gerador está pronto e fará com StopIterationque seja levantado. O valor retornado (se houver) é usado como um 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 ao Python. A idéia geral é que uma função pode manter o estado interno e gerar pontos de dados intermediários sob demanda pelo usuário. Isso prometia ser superior em desempenho para outras abordagens, incluindo o threading do Python , que nem está disponível em alguns sistemas.

  2. Isso significa, por exemplo, que 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 alguém pode recebê-lo. Para enviar dados, é preciso ser capaz de atribuí-lo a algo e, para isso, uma declaração simplesmente não funcionará.


Atalho para o yield Grokking

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

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

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

Não confunda seus Iterables, Iteradores e Geradores

Primeiro, o protocolo do 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) -> isso retorna um objeto com um método next() (ou __next__() no Python 3).

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

  2. Usa o iterador para repetir itens:

    Continue chamando o método next() no iterador retornado da etapa 1. O valor de retorno de next() é atribuído a xeo corpo do loop é executado. Se uma exceção StopIteration for levantada de dentro de 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 - portanto, pode ser um loop for, mas também pode ser código como otherlist.extend(mylist) (onde otherlist é uma lista do Python) .

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

Então esse é o protocolo iterador, muitos objetos implementam esse protocolo:

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

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

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Em vez de instruções yield , se você tivesse três declarações de return em f123() somente a primeira seria executada e a função seria encerrada. 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 realmente não sai - entra em um estado suspenso. Quando o loop for tenta executar um loop sobre o objeto gerador, a função é retomada do estado suspenso na linha seguinte após o yield anterior, executa a próxima linha de código, nesse caso, uma instrução yield e retorna isso como o próximo item. Isso acontece até que a função saia, ponto em que o gerador dispara StopIteration e o loop sai.

Assim, o objeto gerador é mais ou menos como um adaptador - em uma extremidade, exibe o protocolo do iterador, expondo os __iter__() e next() para manter o loop for feliz. No outro extremo, no entanto, ele executa a função apenas o suficiente para obter o próximo valor e a coloca de volta no modo suspenso.

Por que usar geradores?

Normalmente você pode escrever código que não usa geradores, mas implementa a mesma lógica. Uma opção é usar a lista temporária 'truque' que mencionei antes. Isso não funcionará em todos os casos, por exemplo, se você tiver loops infinitos, ou se fizer uso ineficiente de memória quando você tiver uma lista realmente longa. A outra abordagem é implementar uma nova classe iterável, SomethingIter que mantém o estado em 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 método next() pode parecer muito complexo e propenso a erros. Aqui, os geradores fornecem uma solução limpa e fácil.


Para entender o que yield , você deve entender o que são geradores . E antes geradores vêm iterables .

Iterables

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

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

mylist é uma iterável . Quando você usa uma compreensão de lista, você cria uma lista e, portanto, uma 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... " é uma iterável; lists , strings , arquivos ...

Esses iteráveis ​​são úteis porque você pode lê-los tanto quanto desejar, mas armazena todos os valores na memória e isso nem sempre é o que você deseja quando tem muitos valores.

Geradores

Geradores são iteradores, uma espécie de iterável que você só pode repetir uma vez . Geradores não armazenam todos os valores na memória, eles geram os valores em tempo real :

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

É exatamente o mesmo, exceto que você usou () vez de [] . MAS, você não pode executar for i in mygenerator uma segunda vez, já que os geradores só podem ser usados ​​uma vez: eles calculam 0, depois esquecem e calculam 1, e terminam calculando 4, um por um.

Produção

yield é uma palavra chave que é 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 conjunto enorme de valores que você só precisará ler uma vez.

Para dominar o 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 só retorna o objeto gerador, isso é um pouco complicado :-)

Então, seu código será executado toda vez que o for usado pelo gerador.

Agora a parte difícil:

A primeira vez que o for chama o objeto gerador criado a partir de sua função, ele irá executar o código em sua função desde o início até atingir o yield , então retornará o primeiro valor do loop. Em seguida, cada outra chamada executará o loop que você gravou na função mais uma vez e retornará o próximo valor, até que não haja nenhum valor a ser retornado.

O gerador é considerado vazio quando a função é executada, mas não atinge mais o yield . 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 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 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 candidates 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, já que você pode acabar com um loop infinito. Neste caso, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist)) esgota todos os valores do gerador, mas continua criando novos objetos geradores que produzirão valores diferentes dos anteriores já que ele não é aplicado no mesmo nó.

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

Normalmente nós passamos uma lista para isso:

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

Mas no seu código ele recebe 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 quer que eles sejam 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. Python espera iterables, então ele irá trabalhar com strings, listas, tuplas e geradores! Isso é chamado de tipagem de pato e é uma das razões pela qual o Python é tão legal. Mas esta é outra história, para outra pergunta ...

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

Controlando o esgotamento de um 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 o Python 3, use print(corner_street_atm.__next__()) ou print(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? Corrente dois geradores? Agrupar valores em uma lista aninhada com um one-liner? Map / Zip sem criar outra lista?

Em seguida, basta 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

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

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


Pense desta maneira:

Um iterador é apenas um termo sonoro para um objeto que possui um método next (). Então, uma função de rendimento acaba sendo algo assim:

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 interpretador 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 mais informações sobre o que está acontecendo nos bastidores, o loop for 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 notar que isso é uma simplificação excessiva para fins ilustrativos. :)


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é ser chamado e continua exatamente do mesmo ponto em que começou. Você pode obter uma seqüência de todos os valores 'yielded' em um, chamando list(generator()).


O rendimento é um objeto

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

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

Mais importante, yieldé uma barreira .

Como barreira na linguagem CUDA, ela 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é atingir yield. Então, ele retornará o primeiro valor do loop.

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


Ainda outro TL; DR

Iterador na lista : next()retorna o próximo elemento da lista

Gerador de iterador : next()irá calcular o próximo elemento em tempo real (executar código)

Você pode ver o rendimento / gerador como uma forma de executar manualmente o fluxo de controle a partir do exterior (como continue loop one step), chamando next, porém complexo o fluxo.

Nota : O gerador NÃO é uma função normal. Ele lembra o estado anterior como variáveis ​​locais (pilha). Veja outras respostas ou artigos para uma explicação detalhada. O gerador só pode ser iterado uma vez . Você poderia fazer sem yield, mas não seria tão bom, então pode ser considerado um açúcar muito bom.


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)

Saída:

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

Eu não sou um desenvolvedor Python, mas parece-me que yielddetém a posição do fluxo do programa e o próximo ciclo começa a partir da posição "yield". Parece que está esperando nessa posição e, pouco antes disso, retornando um valor para fora e a próxima vez continua funcionando.

Parece ser uma habilidade interessante e agradável: D


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

Além disso, note que yieldpode ser usado em coroutines como o dual de seu uso em funções de gerador. Embora não seja o mesmo uso do seu snippet de código, ele (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 co-rotina será executada até que a próxima (yield)instrução seja encontrada.

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


O rendimento lhe dá 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, foo mantém toda a lista na memória de uma só vez. Não é um grande problema para uma lista com 5 elementos, mas e se você quiser uma lista de 5 milhões? Não só isso é um enorme comedor de memória, também custa muito tempo para construir no momento em que a função é chamada. No segundo caso, a barra apenas fornece um gerador. Um gerador é iterável - o que significa que você pode usá-lo em um loop for, etc, mas cada valor só pode ser acessado uma vez. Todos os valores também não são armazenados na memória ao mesmo tempo; o objeto gerador "lembra" onde estava no loop da última vez que você o chamou - assim, se você estiver usando um iterável para (digamos) contar até 50 bilhões, você não precisa contar até 50 bilhões de uma só vez e armazenar os 50 bilhões de números para contar. Mais uma vez, este é um exemplo bastante artificial,você provavelmente usaria os instrumentos se realmente quisesse contar até 50 bilhões. :)

Este é o caso de uso mais simples dos geradores. Como você disse, ele pode ser usado para escrever permutações eficientes, usando o rendimento para empurrar as coisas através da pilha de chamadas em vez de usar algum tipo de variável de pilha. Os geradores também podem ser usados ​​para percursos especializados de árvores e todas as outras coisas.


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

Quando yieldé usado em vez de uma returnfunçã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 terminarão quando algum valor for retornado dele. Mas com a ajuda do compilador, a função de gerador pode ser considerada como recuperável. Ou seja, o contexto de execução será restaurado e a execução continuará da última execução. Até que você chame explicitamente return, o que gerará uma StopIterationexceção (que também faz parte do protocolo do iterador) ou atingirá o final da função. Eu encontrei um monte de referências sobre generator, mas este onedo functional programming perspectiveé o mais digerível.

(Agora eu quero falar sobre a lógica por trás generator, e com iteratorbase no meu próprio entendimento. Espero que isso possa ajudá-lo a compreender a motivação essencial de iterador e gerador. Tal conceito aparece em outras linguagens, como C #.)

Pelo que entendi, quando queremos processar um monte de dados, geralmente primeiro armazenamos os dados em algum lugar e depois os processamos um por um. Mas essa abordagem intuitiva é problemática. Se o volume de dados é enorme, é caro armazená-los como um todo de antemão. Então, em vez de armazenar o datapróprio diretamente, por que não armazenar algum tipo de metadataindiretamente, isto éthe logic how the data is computed .

Existem duas abordagens para envolver esses metadados.

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

De qualquer maneira, um iterador é criado, isto é, algum objeto que pode fornecer os dados desejados. A abordagem OO pode ser um pouco complexa. De qualquer forma, qual deles usar é com você.


A yieldpalavra-chave simplesmente coleta resultados de retorno. Pense em yieldcomoreturn +=


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

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

Quando você insere isso no REPL e tenta chamá-lo, você obtém um resultado mistificador:

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

Isso ocorre porque a presença de yieldsinalização para o 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 em 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 / produza valores, você "consumirá" o gerador até que nenhum outro valor possa ser produzido por ele (e retorne). :

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 que é preguiçoso. Isso é feito mantendo-se o estado local e permitindo que você reinicie sempre que precisar.

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

f = fib()

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

Quando você solicita que ele gere o primeiro valor, direta ou indiretamente, ele executa todas as declarações que encontrar, até encontrar um yield, então retorna o valor que você forneceu yielde pausa. Para um exemplo que melhor demonstra isso, vamos usar algumas printchamadas (substitua por print "text"se 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 esperando por 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 o que é impresso. 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 pausado yield valuee retoma a partir daí. A próxima mensagem é impressa e a procura pela yieldinstrução para pausar é executada novamente (devido ao whileloop).


Aqui está uma imagem mental do que yieldfaz.

Eu gosto de pensar em um segmento como tendo uma pilha (mesmo quando não é implementado dessa forma).

Quando uma função normal é chamada, ela coloca suas variáveis ​​locais na pilha, faz alguma computação, 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 rodar (isto é, depois que a função é chamada, retornando um objeto gerador, cujo next()método é invocado), ele coloca suas variáveis ​​locais na pilha e calcula por um tempo. Mas então, quando atinge a yielddeclaração, antes de limpar sua parte da pilha e retornar, ela pega um instantâneo de suas variáveis ​​locais e as armazena no objeto gerador. Ele também escreve o lugar onde está atualmente em seu código (ou seja, a yielddeclaração particular ).

Então é uma espécie de função congelada que o gerador está pendurado.

Quando next()é chamado posteriormente, ele recupera os pertences da função na pilha e a re-anima. A função continua a calcular de onde parou, alheia ao fato de que ela havia passado uma eternidade no armazenamento a frio.

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 ser inacessível, mas se estiver presente em qualquer lugar, muda a natureza daquilo com que estamos lidando.

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

Chamar yielderFunction()não executa seu código, mas faz um gerador fora do código. (Talvez seja uma boa ideia 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 com dir(..), podemos confirmar que nosso modelo mental acima é credível.


Do ponto de vista de 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), um usa mensagens enviadas a um objeto de fechamento, que possui um dispatcher, e o dispatcher responde a "mensagens".

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

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

Existem muitas 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 yielder.

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 alteração na sintaxe é necessária para reescrevê-lo 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
->

Em resumo, a yieldinstrução transforma sua função em uma fábrica que produz um objeto especial chamado a generatorque envolve o corpo de sua função original. Quando generatoriterated, ele executa sua função até que ela atinja a próxima yield, suspendendo a execução e avaliando o valor passado 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 saídas

one
two
three

A energia vem do uso do gerador com um loop que calcula uma sequência, o gerador executa o loop parando cada vez para 'render' o próximo resultado do cálculo, desta forma ele calcula uma lista na hora, sendo o benefício a memória salvo para cálculos especialmente grandes

Digamos que você quisesse criar uma rangefunção própria que produzisse um intervalo iterável de números, você poderia fazer assim,

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

e usá-lo 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)
  • Este código, na verdade, 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, em cada iteração, uma função no gerador chamado next()executa a função até chegar a uma instrução de 'rendimento' na qual ela pára e 'rende' o valor ou atinge o final da função. Neste caso na primeira chamada, next()executa até a declaração de rendimento e produz 'n', na próxima chamada executará a instrução de incremento, retornará ao 'while', avaliá-lo e se verdadeiro, parará e yield 'n' de novo, continuará assim até que a condição while retorne false e o gerador pule para o final da função.


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

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


Existe outro yielduso e significado (desde o Python 3.3):

yield from <expr>

De 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 para o gerador de delegação.

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

Além disso, this introduzirá (desde o Python 3.5):

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

para evitar que corrotinas sejam confundidas com um gerador regular (hoje yieldé usado em ambos).


Há uma coisa extra para mencionar: 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 o trabalho com algumas coisas.


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

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

retorno (na 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 yieldusa três linhas em vez de cinco e tem uma variável a menos para se preocupar.

Este é o resultado do código:

Como você pode ver as duas funções fazem a mesma coisa. A única diferença é return_dates()dar uma lista e yield_dates()dá um gerador.

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





coroutine