python geradores O que a palavra-chave "yield" faz?




yield python (24)

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.

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 .


Aqui estão alguns exemplos de Python de como 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)

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

Para ajudar a entender o que yieldfaz no código a seguir, você pode usar o dedo para rastrear o ciclo por meio de qualquer código que tenha um yield. Toda vez que seu dedo tocar yield, você terá que esperar que a nextou senda seja inserido. Quando a nexté chamado, você rastreia o código até atingir o yield… o código à direita do yieldavaliado é avaliado e retornado ao responsável pela chamada… e então você espera. Quando nexté chamado novamente, você executa outro loop através do código. No entanto, você notará que em uma co-rotina, yieldtambém pode ser usado com um send… que enviará um valor do chamador para a função de produção. Se a sendé dado, entãoyieldrecebe o valor enviado, e o cospe do lado esquerdo… então o rastreamento pelo código progride até você acertar 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()

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

A yielddeclaração no Python retorna um gerador. Um gerador em Python é uma função que retorna continuações (e especificamente um tipo de co-rotina, mas as 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 elas não são usadas com frequência, porque são extremamente difíceis de raciocinar e também muito difíceis de implementar. Mas a ideia 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 e assim por diante são salvos. Então, em algum momento mais adiante no programa, a continuação pode ser invocada, de tal forma que as variáveis ​​do programa são redefinidas para esse estado e as operações que foram salvas são executadas.

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 é invocada, a pilha é restaurada.

No continuation passing style (CPS), as continuações são apenas funções normais (somente em linguagens onde as funções são de primeira classe) que o programador gerencia explicitamente e passa para as sub-rotinas. Nesse estilo, o estado do programa é representado por closures (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 invocando-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 realmente gravar o arquivo em uma continuação (que pode potencialmente ser uma operação muito complexa com muitos detalhes para escrever) e, em seguida, passa essa continuação (ou seja, como um primeiro fechamento de classe) para outro operador que faz mais algum processamento e, em seguida, chama, se necessário. (Eu uso muito esse padrão de design na programação da GUI, seja porque ele me salva linhas de código ou, mais importante, para gerenciar o fluxo de controle após o acionamento dos eventos da GUI.)

O resto deste post irá, 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. Considerando que as continuações são capazes, em geral, de salvar o estado de uma computação (ou seja, a pilha de chamadas do programa), os geradores são capazes apenas de salvar o estado da iteração sobre um iterador . Embora essa definição seja um pouco enganosa para certos casos de uso de geradores. Por exemplo:

def f():
  while True:
    yield 4

Este é claramente um iterável razoável cujo comportamento é bem definido - cada vez que o gerador itera sobre ele, ele retorna 4 (e faz isso para sempre). Mas não é provavelmente o tipo prototípico de iterável que vem à mente quando se pensa em iteradores (ou seja, for x in collection: do_something(x)). Este exemplo ilustra o poder dos geradores: se alguma coisa é 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 mais poderosas que os geradores, mas também que os geradores são muito mais fáceis. Eles são mais fáceis para o designer de linguagem implementar, e são mais fáceis para o programador usar (se você tiver algum tempo para gravar, tente ler e entender esta página sobre continuações e chamar / cc ).

Mas você poderia facilmente implementar (e conceitualizar) 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 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 usar a yieldpalavra - chave.


yieldé como return- retorna tudo o que você diz (como um gerador). A diferença é que na próxima vez que você chamar o gerador, a execução será iniciada da última chamada para a yieldinstrução. Ao contrário 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.

No caso do seu código, a função get_child_candidatesestá agindo como um iterador para que, ao estender sua lista, ele adicione 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 apenas retornar uma tupla e anexá-la à lista.


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


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


A palavra-chave yield é reduzida para dois fatos simples:

  1. Se o compilador detectar a palavra-chave yield qualquer lugar dentro de uma função, essa função não retornará mais pela declaração de return . Em vez disso , ele imediatamente retorna um objeto "lista pendente" preguiçoso chamado gerador
  2. Um gerador é iterável. O que é um iterável ? É qualquer coisa como uma list ou set ou range ou visualização dita, com um protocolo interno para visitar cada elemento em uma determinada ordem .

Resumindo: um gerador é uma lista preguiçosa, com incrementos pendentes , e as declarações de yield permitem que você use a notação de função para programar os valores da lista que o gerador deve cunhar de forma incremental.

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 makeRange que é como o range do Python. Chamando 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() (assim como você faria qualquer 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 que você acrescenta 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 de uma compreensão de lista, e todos os geradores são iteráveis, então eles são freqüentemente 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 módulo itertools (certifique-se de usar chain.from_iterable vez de chain quando necessário). Por exemplo, você pode até usar geradores para implementar listas itertools.count() infinitamente longas como itertools.count() . Você poderia implementar seu próprio def enumerate(iterable): zip(count(), iterable) ou, alternativamente, fazê-lo com a palavra-chave yield em um loop while.

Por favor note: geradores podem realmente ser usados ​​para muitas outras coisas, como a implementação de coroutines ou programação não-determinística ou outras coisas elegantes. No entanto, o ponto de vista de "listas preguiçosas" que apresento aqui é o uso mais comum que você encontrará.

Por trás das cenas

É assim que funciona o "protocolo de iteração Python". Isto é, o que está acontecendo quando você faz uma list(makeRange(5)) . Isto é o que eu descrevo anteriormente como uma "lista incremental preguiçosa".

>>> 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() next() apenas chama a função objetos .next() , que faz parte do "protocolo de iteração" e é encontrada em todos os iteradores. Você pode manualmente usar a função next() (e outras partes do protocolo de iteração) para implementar coisas sofisticadas, geralmente à custa da legibilidade, então tente evitar fazer isso ...

Minúcias

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

No Python-speak, um iterável é qualquer objeto que "entende o conceito de um loop for" como uma lista [1,2,3] , e um iterador é uma instância específica do loop for solicitado como [1,2,3].__iter__() . Um gerador é exatamente igual a qualquer iterador, exceto pelo modo como foi escrito (com sintaxe de 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 apenas fornece uma cópia de si mesmo.

Assim, no caso improvável de você não estar fazendo algo assim ...

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

... então lembre-se de que um gerador é um iterador ; isto é, é um uso único. Se você quiser reutilizá-lo, você deve chamar 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 metaprogramação hackeana) podem usar o itertools.tee se for absolutamente necessário, uma vez que a proposta de padrões Python PEP iterador copiável foi adiada.


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.


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 .


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

Eu quero operar em uma seqüência de números, mas eu não quero me incomodar com a criação dessa seqüência, quero apenas focar na operação que quero fazer. Então, eu faço o seguinte:

  • Eu te chamo e te digo que eu quero uma sequência de números que é produzida de uma maneira específica, e eu deixo você saber qual é o algoritmo.
    Este passo corresponde à deffunção gerador, isto é, a função contendo a yield.
  • Algum tempo depois, eu lhe digo: "OK, prepare-se para me contar a sequência de números".
    Este passo corresponde a chamar a função geradora que retorna um objeto gerador. Note que você não me conta nenhum número ainda; você acabou de pegar seu papel e lápis.
  • Eu lhe pergunto, "diga-me o próximo número", e você me diz o primeiro número; depois disso, você espera que eu lhe peça o próximo número. O seu trabalho é lembrar onde você estava, que números você já disse e qual é o próximo número. Eu não me importo com os detalhes.
    Este passo corresponde a chamar .next()o 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! Estou pronto! Sem mais números!"
    Esta etapa corresponde ao objeto gerador que finaliza seu trabalho e gera uma StopIterationexceção. A função geradora não precisa levantar a exceção. É gerado automaticamente quando a função termina ou emite um return.

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

O usuário mais famoso do protocolo do iterador é o forcomando em Python. Então, sempre que você fizer 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ê itens de uma sequência, um por um.

Note que defuma 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 um.

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


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.


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


TL; DR

Em vez disso:

def squares_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 squares_the_yield_way(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Sempre que você se encontrar construindo uma lista a partir do zero, yieldcada peça em vez disso.

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 squares_list(4):
...     print(square)
...
0
1
4
9
>>> for square in squares_the_yield_way(4):
...     print(square)
...
0
1
4
9

Comportamento diferente:

O rendimento é de passagem única : você só pode percorrer uma vez. Quando uma função tem um rendimento, chamamos de função geradora . E um iterator é o que ele retorna. Isso é revelador. Perdemos a conveniência de um contêiner, mas ganhamos o poder de uma série arbitrariamente longa.

O rendimento é preguiçoso , adia a computação. Uma função com um rendimento não é realmente executada quando você a chama. O objeto iterador retornado usa magic para manter o contexto interno da função. Cada vez que você chama next()a execução do iterador (isso acontece em um loop), avança para o próximo rendimento. ( returnlevanta StopIteratione termina a série.)

O rendimento é versátil . Pode fazer loops infinitos:

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

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

Escolha brilhante da palavra yieldporque ambos os significados se aplicam:

rendimento - produzir ou fornecer (como na agricultura)

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

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

... abdicar da execução da CPU até que o iterador avance.


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.


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


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

Eu suponho que você tenha aprendido a returnafirmação.

Como analogia, returne yieldsão gêmeos. returnsignifica "retornar e parar", enquanto "rendimento" significa "retorno, mas continua"

  1. Tente obter uma 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 pára, yieldexecuta vezes que você planejou. Você pode interpretar returncomo return one of theme yieldcomo return all of them. Isso é chamado iterable.

  1. Mais um passo, 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 uma returnsaída de 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. Então, ele tem um novo generatorobjeto de nome como exibido em Out[11]: <generator object num_list at 0x10327c990>.

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

  • returne yieldsão gêmeos
  • liste generatorsão gêmeos

Como toda resposta sugere, yieldé usado para criar um gerador de seqüê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)

Captcha 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 () é invocado, a execução começa a partir do ponto em que foi pausada da última vez.

Assim, em suma, 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

vai imprimir

"first time"
"second time"
"third time"
"Now some useful value 12"

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

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.


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.


Para aqueles que preferem um exemplo mínimo de trabalho, 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

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.







coroutine