O que a palavra-chave “rendimento” faz?
Pergunta
Qual é a utilidade do yield
palavra-chave em Python?O que isso faz?
Por exemplo, estou tentando entender esse código1:
def _get_child_candidates(self, distance, min_dist, max_dist):
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
E este é o chamador:
result, candidates = [], [self]
while candidates:
node = candidates.pop()
distance = node._get_dist(obj)
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
O que acontece quando o método _get_child_candidates
é chamado?Uma lista é retornada?Um único elemento?É chamado de novo?Quando as chamadas subsequentes serão interrompidas?
1.Este trecho de código foi escrito por Jochen Schulz (jrschulz), que criou uma excelente biblioteca Python para espaços métricos.Este é o link para a fonte completa: Módulo mspace.
Solução
Para entender o que yield
faz, você deve entender o que geradores são.E antes que você possa entender os geradores, você deve entender iteráveis.
Iteráveis
Ao criar uma lista, você pode ler seus itens um por um.Ler seus itens um por um é chamado de iteração:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
é um iterável.Ao usar uma compreensão de lista, você cria uma lista e, portanto, um iterável:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Tudo que você pode usar "for... in...
"on é um iterável; lists
, strings
, arquivos...
Esses iteráveis são úteis porque você pode lê-los o quanto quiser, mas armazena todos os valores na memória e nem sempre é isso que você deseja quando tem muitos valores.
Geradores
Geradores são iteradores, uma espécie de iterável você só pode iterar uma vez.Os geradores não armazenam todos os valores na memória, eles geram os valores instantaneamente:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
É exatamente a mesma coisa, exceto que você usou ()
em 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.
Colheita
yield
é uma palavra-chave usada como return
, exceto que a função retornará um gerador.
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Aqui está um exemplo inútil, mas é útil quando você sabe que sua função retornará um conjunto enorme de valores que você só precisará ler uma vez.
Dominar yield
, você deve entender isso quando você chama a função, o código escrito no corpo da função não é executado. A função retorna apenas o objeto gerador, isso é um pouco complicado :-)
Então, seu código continuará de onde parou todas as vezes for
usa o gerador.
Agora a parte difícil:
A primeira vez que for
chama o objeto gerador criado a partir da sua função, ele executará o código da sua função desde o início até atingir yield
, então ele retornará o primeiro valor do loop.Então, cada outra chamada executará o loop que você escreveu na função mais uma vez e retornará o próximo valor, até que não haja mais valor para retornar.
O gerador é considerado vazio quando a função é executada, mas não atinge yield
não mais.Pode ser porque o loop chegou ao fim ou porque você não satisfaz uma "if/else"
não mais.
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, pois você pode acabar com um loop infinito.Nesse caso,
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
esgota todos os valores do gerador, maswhile
continua criando novos objetos geradores que produzirão valores diferentes dos anteriores, pois não são aplicados no mesmo nó.O
extend()
método é um método de objeto de lista que espera um iterável e adiciona seus valores à lista.
Normalmente passamos uma lista para ele:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Mas no seu código ganha um gerador, o que é bom porque:
- Você não precisa ler os valores duas vezes.
- Você pode ter muitos filhos e não querer que todos 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 iteráveis para funcionar com strings, listas, tuplas e geradores!Isso é chamado de digitação de pato e é uma das razões pelas quais o Python é tão legal.Mas isso é outra história, para outra pergunta...
Você pode parar por aqui ou ler um pouco para ver um uso avançado de um gerador:
Controlando a exaustão 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
...
Observação: Para Python 3, useprint(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.Você já desejou duplicar um gerador?Encadear dois geradores?Agrupar valores em uma lista aninhada com uma linha? Map / Zip
sem criar outra lista?
Então é só import itertools
.
Um exemplo?Vejamos 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)]
Compreendendo os mecanismos internos da iteração
Iteração é um processo que implica iteráveis (implementando o __iter__()
método) e iteradores (implementando o __next__()
método).Iteráveis são quaisquer objetos dos quais você pode obter um iterador.Iteradores são objetos que permitem iterar em iteráveis.
Há mais sobre isso neste artigo sobre como for
loops funcionam.
Outras dicas
Atalho para entender yield
Quando você vê uma função com yield
Declarações, aplique esse truque fácil de entender o que vai acontecer:
- Insira uma linha
result = []
No início da função. - Substitua cada um
yield expr
comresult.append(expr)
. - Insira uma linha
return result
na parte inferior da função. - Yay - não mais
yield
declarações! Leia e descubra o código. - 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 yield
é significativamente diferente que o que acontece na abordagem baseada na lista. Em muitos casos, a abordagem de rendimento também será muito mais eficiente e mais rápida na memória. Em outros casos, esse truque o deixará preso em um loop infinito, mesmo que a função original funcione bem. Continue lendo para saber mais ...
Não confunda seus iteráveis, iteradores e geradores
Primeiro, o Protocolo do iterador - Quando você escreve
for x in mylist:
...loop body...
Python executa as duas etapas a seguir:
Recebe um iterador para
mylist
:Ligar
iter(mylist)
-> Isso retorna um objeto com umnext()
Método (ou__next__()
em Python 3).Este é o passo que a maioria das pessoas esquece de falar sobre você
Usa o iterador para dar um loop sobre os itens:
Continue ligando para o
next()
método no iterador retornado da etapa 1. o valor de retorno denext()
é atribuído ax
e o corpo do loop é executado. Se uma exceçãoStopIteration
é criado de dentronext()
, isso significa que não há mais valores no iterador e o loop é excitado.
A verdade é que Python executa as duas etapas acima sempre que quiser Faça um loop o conteúdo de um objeto - para que possa ser um loop para otherlist.extend(mylist)
(Onde otherlist
é uma lista de Python).
Aqui mylist
é um iterável Porque implementa o protocolo do iterador. Em uma classe definida pelo usuário, você pode implementar o __iter__()
Método para tornar as instâncias da sua classe iterável. Este método deve retornar um iterador. Um iterador é um objeto com um next()
método. É possível implementar ambos __iter__()
e next()
na mesma classe, e tenho __iter__()
Retorna self
. Isso funcionará para casos simples, mas não quando você deseja que dois iteradores sejam entregues sobre o mesmo objeto ao mesmo tempo.
Então esse é o protocolo Iterator, muitos objetos implementam este protocolo:
- Listas internas, dicionários, tuplas, conjuntos, arquivos.
- Classes definidas pelo usuário que implementam
__iter__()
. - Geradores.
Observe que a for
O loop não sabe com que tipo de objeto está lidando - apenas segue o protocolo do iterador e fica feliz em obter item após item, conforme ele chama next()
. Listas embutidas retornam seus itens um por um, os dicionários retornam o chaves um por um, os arquivos retornam o linhas um por um, etc. e os geradores retornam ... bem, é onde yield
entra:
def f123():
yield 1
yield 2
yield 3
for item in f123():
print item
Ao invés de yield
declarações, se você tivesse três return
declarações em f123()
Somente o primeiro seria executado e a função sairia. Mas f123()
não é uma função comum. Quando f123()
é chamado, isso não Retorne qualquer um dos valores nas declarações de rendimento! Ele retorna um objeto gerador. Além disso, a função realmente não sai - ela entra em um estado suspenso. Quando o for
Loop tenta fazer loop sobre o objeto do gerador, a função retoma de seu estado suspenso na próxima linha após o yield
Ele retornou anteriormente, executa a próxima linha de código, neste caso um yield
declaração, e retorna isso como o próximo item. Isso acontece até que a função saia, momento em que o gerador aumenta StopIteration
, e o loop sai.
Portanto, o objeto do gerador é como um adaptador - em uma extremidade, exibe o protocolo Iterator, expondo __iter__()
e next()
métodos para manter o for
Loop feliz. No outro lado, no entanto, ele executa a função apenas o suficiente para obter o próximo valor e coloca -o de volta no modo suspenso.
Por que usar geradores?
Geralmente você pode escrever código que não usa geradores, mas implementa a mesma lógica. Uma opção é usar o 'truque' da lista temporária que mencionei antes. Isso não funcionará em todos os casos, por exemplo, se você tiver loops infinitos, ou pode fazer uso ineficiente da memória quando tiver uma lista muito longa. A outra abordagem é implementar uma nova classe iterável SomethingIter
isso mantém o estado, por exemplo, membros e executa a próxima etapa lógica em item next()
(ou __next__()
no Python 3) Método. Dependendo da lógica, o código dentro do next()
O método pode acabar parecendo muito complexo e propenso a bugs. Aqui os geradores fornecem uma solução limpa e fácil.
Pense desta maneira:
Um iterador é apenas um termo de som chique para um objeto que tem um next()
método. Então, uma função de edição do 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 intérprete Python faz com o código acima:
class it:
def __init__(self):
# Start at -1 so that we get 0 when we add 1 below.
self.count = -1
# The __iter__ method will be called once by the 'for' loop.
# The rest of the magic happens on the object returned by this method.
# In this case it is the object itself.
def __iter__(self):
return self
# The next method will be called repeatedly by the 'for' loop
# until it raises StopIteration.
def next(self):
self.count += 1
if self.count < 4:
return self.count
else:
# A StopIteration exception is raised
# to signal that the iterator is done.
# This is caught implicitly by the 'for' loop.
raise StopIteration
def some_func():
return it()
for i in some_func():
print i
Para mais informações sobre o que está acontecendo nos bastidores, o for
O loop 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? :)
Eu deveria observar que isso é uma simplificação excessiva para fins ilustrativos. :)
O yield
palavra-chave é reduzida a dois fatos simples:
- Se o compilador detectar o
yield
palavra-chave em qualquer lugar dentro de uma função, essa função não retorna mais através doreturn
declaração. Em vez de, isto imediatamente retorna um objeto preguiçoso "lista pendente" chamado de gerador - Um gerador é iterável.O que é um iterável?É qualquer coisa como um
list
ouset
ourange
ou dict-view, com um protocolo integrado para visitar cada elemento em uma determinada ordem.
Resumindo: um gerador é uma lista preguiçosa e pendente de forma incremental, e yield
instruções permitem que você use notação de função para programar os valores da lista o gerador deve cuspir gradativamente.
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
isso é igual ao do Python range
.Chamando makeRange(n)
DEVOLVE 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 com 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 uma simples criação de uma lista que você anexa e retorna:
# list-version # # generator-version
def makeRange(n): # def makeRange(n):
"""return [0,1,2,...,n-1]""" #~ """return 0,1,2,...,n-1"""
TO_RETURN = [] #>
i = 0 # i = 0
while i < n: # while i < n:
TO_RETURN += [i] #~ yield i
i += 1 # i += 1 ## indented
return TO_RETURN #>
>>> makeRange(5)
[0, 1, 2, 3, 4]
Porém, há uma grande diferença;veja a última seção.
Como você pode usar geradores
Um iterável é a última parte da compreensão de uma lista, e todos os geradores são iteráveis, por isso são frequentemente usados assim:
# _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]
Para ter uma ideia melhor dos geradores, você pode brincar com o itertools
módulo (certifique-se de usar chain.from_iterable
em vez de chain
quando justificado).Por exemplo, você pode até usar geradores para implementar listas preguiçosas infinitamente longas, como itertools.count()
.Você poderia implementar seu próprio def enumerate(iterable): zip(count(), iterable)
, ou alternativamente fazê-lo com o yield
palavra-chave em um loop while.
Observe:geradores podem realmente ser usados para muito mais coisas, como implementando corrotinas ou programação não determinística ou outras coisas elegantes.No entanto, o ponto de vista das “listas preguiçosas” que apresento aqui é o uso mais comum que você encontrará.
Por trás das cenas
É assim que funciona o "protocolo de iteração Python".Isto é, o que está acontecendo quando você faz list(makeRange(5))
.Isso é o que descrevi anteriormente como uma "lista incremental e 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 integrada next()
apenas chama os objetos .next()
função, que faz parte do "protocolo de iteração" e é encontrada em todos os iteradores.Você pode usar manualmente o next()
função (e outras partes do protocolo de iteração) para implementar coisas sofisticadas, geralmente às custas da legibilidade, então tente evitar fazer isso...
Minúcias
Normalmente, a maioria das pessoas não se importaria com as distinções a seguir e provavelmente gostaria de parar de ler aqui.
Na linguagem Python, um iterável é qualquer objeto que "entende o conceito de loop for" como uma lista [1,2,3]
, e um iterador é uma instância específica do loop for solicitado, como [1,2,3].__iter__()
.A gerador é exatamente igual a qualquer iterador, exceto pela forma 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 raramente faria), ele apenas fornece uma cópia de si mesmo.
Assim, no caso improvável de você não conseguir fazer algo assim...
> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]
...então lembre-se que um gerador é um iterador;isto é, é de uso único.Se você quiser reutilizá-lo, você deve ligar myRange(...)
de novo.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 realmente precisam clonar um gerador (por exemplo, que estão fazendo uma metaprogramação terrivelmente hackeada) podem usar itertools.tee
se for absolutamente necessário, já que o iterador copiável Python PEP proposta de padrões foi adiada.
O que faz o
yield
Palavra -chave em Python?
Resposta Esboço/Resumo
- Uma função com
yield
, quando chamado, retorna a Gerador. - Geradores são iteradores porque implementam o Protocolo do iterador, para que você possa iterar sobre eles.
- Um gerador também pode ser Informações enviadas, tornando -o conceitualmente um Coroutina.
- Em 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 idéia para geradores vem de outros idiomas (consulte a nota de rodapé 1) com implementações variadas. Nos geradores de Python, a execução do código é congeladas no ponto do rendimento. Quando o gerador é chamado (os métodos são discutidos abaixo) currículos de execução e depois congela no próximo rendimento.
yield
fornece uma maneira fácil de Implementando o Protocolo 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 verificar com o Iterator
Classe base abstrata da collections
módulo.
>>> def func():
... yield 'I am'
... yield 'a generator!'
...
>>> type(func) # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen) # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__') # that's an iterable
True
>>> hasattr(gen, 'next') # and with .next (.__next__ in Python 3)
True # implements the iterator protocol.
O tipo de gerador é um subtipo de iterador:
>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True
E se necessário, podemos verificar assim:
>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True
Uma característica de um Iterator
isso está exausto, você não pode reutilizar 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!']
Pode -se produzir dados programaticamente, por exemplo:
def func(an_iterable):
for item in an_iterable:
yield item
O gerador simples acima também é equivalente ao abaixo - a partir do Python 3.3 (e não está disponível no Python 2), você pode usar yield from
:
def func(an_iterable):
yield from an_iterable
No entanto, yield from
Também permite a delegação aos subgeneradores, que serão explicados na seção a seguir sobre delegação cooperativa com sub-corototinas.
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 do received
Variável, que apontará para os dados enviados ao gerador:
def bank_account(deposited, interest_rate):
while True:
calculated_interest = interest_rate * deposited
received = yield calculated_interest
if received:
deposited += received
>>> my_account = bank_account(1000, .05)
Primeiro, devemos fazer fila o gerador com a função incorporada, next
. Ele chamará o apropriado next
ou __next__
Método, dependendo da versão do Python que você está usando:
>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0
E agora podemos enviar dados para o gerador. (Envio None
é o mesmo que chamar next
.) :
>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5
Delegação cooperativa para sub-coroar yield from
Agora, lembre -se disso yield from
está disponível no Python 3. Isso nos permite delegar coroutines a uma subcorutina:
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 a funcionalidade a um sub-gerador e ela pode ser usada 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
dentro PEP 380.
Outros métodos: feche e jogue
o close
Método aumenta GeneratorExit
No momento, a execução da função foi congelada. Isso também será chamado por __del__
Então você pode colocar qualquer código de limpeza onde você lida com o GeneratorExit
:
>>> my_account.close()
Você também pode lançar uma exceção que pode ser tratada no gerador ou propagada de volta ao 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
Eu acredito que abordei todos os aspectos da seguinte pergunta:
O que faz o
yield
Palavra -chave em Python?
Acontece que yield
faz muito. Tenho certeza de que poderia adicionar exemplos ainda mais completos a isso. Se você quiser mais ou tiver algumas críticas construtivas, me avise com o comentário abaixo.
Apêndice:
Crítica da resposta superior/aceita **
- Está confuso sobre o que faz um iterável, apenas usando uma lista como exemplo. Veja minhas referências acima, mas em resumo: um iterável tem um
__iter__
Método retornando um iterador. Um iterador Fornece uma.next
(Python 2 ou.__next__
(Python 3) Método, que é implicitamente chamado porfor
loops até aumentarStopIteration
, E uma vez que isso acontecer, 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, apenas confunde o assunto, e ainda não chegamos ao
yield
papel. - Dentro Controlando uma exaustão de um gerador Ele liga para o
.next
método, quando ele deve usar a função incorporada,next
. Seria uma camada de indireção apropriada, porque seu código não funciona no Python 3. - Itetools? Isso não era relevante para o que
yield
sim. - Nenhuma discussão sobre os métodos que
yield
fornece junto com a nova funcionalidadeyield from
em Python 3. A resposta superior/aceita é uma resposta muito incompleta.
Crítica de resposta sugerindo yield
em uma expressão ou compreensão do gerador.
Atualmente, a gramática permite qualquer expressão em uma compreensão da 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, ele foi apresentado por alguns como interessantes usá -lo em compreensões ou expressão do gerador - apesar de citar nenhum caso de uso particularmente bom.
Os desenvolvedores do CPython Core são discutindo depreciar seu subsídio. Aqui está uma postagem relevante da lista de discussão:
Em 30 de janeiro de 2017 às 19:05, Brett Cannon escreveu:
On Sun, 29 de janeiro de 2017 às 16:39 Craig Rodrigues escreveu:
Estou bem com qualquer abordagem. Deixar as coisas do jeito que estão no Python 3 não é bom, IMHO.
Meu voto é ser um SyntaxError, pois você não está recebendo o que espera da sintaxe.
Eu concordo que é um lugar sensato para acabarmos, pois qualquer código que confia no comportamento atual é realmente muito inteligente para ser mantido.
Em termos de chegar lá, provavelmente queremos:
- Sintaxe ou depreciação em 3.7
- Aviso PY3K em 2.7.x
- SyntaxError em 3.8
Saúde, Nick.
- Nick Coghlan | ncoghlan em gmail.com | Brisbane, Austrália
Além disso, existe um Excelente edição (10544) o que parece estar apontando na direção disso Nunca Ser uma boa idéia (Pypy, uma implementação do Python escrita em Python, já está levantando avisos de sintaxe.)
Resumindo, até que os desenvolvedores da Cpython nos dizem o contrário: Não coloque yield
em uma expressão ou compreensão do gerador.
o return
declaração em um gerador
Dentro Python 2:
Em uma função de gerador, o
return
A declaração não pode incluir umexpression_list
. Nesse contexto, um nureturn
indica que o gerador está feito e causaráStopIteration
ser criado.
Um expression_list
é basicamente qualquer número de expressões separadas por vírgulas - essencialmente, em Python 2, você pode parar o gerador com return
, mas você não pode retornar um valor.
Dentro Python 3:
Em uma função de gerador, o
return
a declaração indica que o gerador é feito e causaráStopIteration
ser criado. O valor retornado (se houver) é usado como um argumento para construirStopIteration
e se torna oStopIteration.value
atributo.
Notas de rodapé
Os idiomas Clu, Sather e Icon foram referenciados na proposta de introduzir o conceito de geradores ao Python. A idéia geral é que uma função possa manter o estado interno e produzir pontos de dados intermediários sob demanda pelo usuário. Isso prometeu ser Superior no desempenho de outras abordagens, incluindo Threading Python, que nem sequer está disponível em alguns sistemas.
Isso significa, por exemplo, que
xrange
objetos (range
em Python 3) não sãoIterator
S, mesmo que sejam iteráveis, porque podem ser reutilizados. Como listas, suas__iter__
Métodos retornam objetos do iterador.yield
foi 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. Agorayield
cria uma expressão de rendimento.https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Essa mudança foi proposto Para permitir que um usuário envie dados para o gerador, assim como um pode recebê -los. Para enviar dados, é preciso ser capaz de atribuí -los a algo e, para isso, uma declaração simplesmente não funcionará.
yield
é como return
- ele retorna o que você diz para (como gerador). A diferença é que na próxima vez que você ligar para o gerador, a execução começa da última chamada para o yield
declaraçã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, para que seu estado seja retomado na próxima vez que a função for chamada.
No caso do seu código, a função get_child_candidates
está agindo como um iterador para que, quando você estenda sua lista, ele adiciona um elemento de cada vez à nova lista.
list.extend
chama um iterador até que esteja exausto. No caso do exemplo de código que você postou, seria muito mais claro retornar uma tupla e anexá -la à lista.
Há uma coisa extra a mencionar: uma função que os rendimentos não precisam finalizar. Eu escrevi código como este:
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.
Para aqueles que preferem um exemplo de trabalho mínimo, medite nesta sessão interativa do Python:
>>> def f():
... yield 1
... yield 2
... yield 3
...
>>> g = f()
>>> for i in g:
... print i
...
1
2
3
>>> for i in g:
... print i
...
>>> # Note that this time nothing was printed
Tl; dr
Em vez disso:
def square_list(n):
the_list = [] # Replace
for x in range(n):
y = x * x
the_list.append(y) # these
return the_list # lines
fazem isto:
def square_yield(n):
for x in range(n):
y = x * x
yield y # with this one.
Sempre que você se encontra construindo uma lista do zero, yield
Cada peça em vez disso.
Este foi o meu primeiro momento "Aha" com rendimento.
yield
é um açucarado maneira de dizer
Construa uma série de coisas
Mesmo comportamento:
>>> for square in square_list(4):
... print(square)
...
0
1
4
9
>>> for square in square_yield(4):
... print(square)
...
0
1
4
9
Comportamento diferente:
Rendimento é passagem única: Você só pode iterar uma vez. Quando uma função tem um rendimento, chamamos de Função do gerador. E um iterador é o que ele retorna. Esses termos são reveladores. Perdemos a conveniência de um contêiner, mas obtemos o poder de uma série calculada conforme necessário e arbitrariamente por muito tempo.
Rendimento é preguiçoso, ele adia a computação. Uma função com um rendimento nele Na verdade, não é executado quando você chama. Ele retorna um objeto iterador Isso se lembra de onde parou. Cada vez que você liga next()
No iterador (isso acontece em um loop para o loop), a execução de uma polegada para o próximo rendimento. return
levanta a parada e termina a série (este é o fim natural de um loop para o forro).
Rendimento é versátil. Os dados não precisam ser armazenados juntos, podem ser disponibilizados um de cada vez. Pode ser infinito.
>>> def squares_all_of_them():
... x = 0
... while True:
... yield x * x
... x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
... print(next(squares))
...
0
1
4
9
Se você precisar Passes múltiplos E a série não é muito longa, basta ligar list()
nele:
>>> list(square_yield(4))
[0, 1, 4, 9]
Brilhante escolha da palavra yield
Porque ambos os significados Aplique:
colheita - produzir ou fornecer (como na agricultura)
... forneça os próximos dados da série.
colheita - Dê um caminho ou abandone (como no poder político)
... renunciar à execução da CPU até que o iterador avança.
O rendimento fornece um gerador.
def get_odd_numbers(i):
return range(1, i, 2)
def yield_odd_numbers(i):
for x in range(1, i, 2):
yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5
Como você pode ver, no primeiro caso foo
mantém a lista inteira na memória de uma só vez.Não é grande coisa para uma lista com 5 elementos, mas e se você quiser uma lista de 5 milhões?Isso não apenas consome muita memória, mas também leva muito tempo para ser construído no momento em que a função é chamada.
No segundo caso, bar
apenas lhe dá um gerador.Um gerador é iterável - o que significa que você pode usá-lo em um for
loop, 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 - desta forma, se você estiver usando um iterável para (digamos) contar até 50 bilhões, você não precisa contar até 50 bilhões no total de uma só vez e armazene os 50 bilhões de números para contar.
Novamente, este é um exemplo bastante inventado, você provavelmente usaria itertools se realmente quisesse contar até 50 bilhões.:)
Este é o caso de uso mais simples de geradores.Como você disse, ele pode ser usado para escrever permutações eficientes, usando rendimento para empurrar as coisas para cima na pilha de chamadas, em vez de usar algum tipo de variável de pilha.Os geradores também podem ser usados para travessia especializada de árvores e todo tipo de outras coisas.
Está devolvendo um gerador.Não estou particularmente familiarizado com Python, mas acredito que seja o mesmo tipo de coisa que Blocos iteradores do C# se você estiver familiarizado com eles.
A ideia principal é que o compilador/interpretador/o que quer que seja, faça alguns truques para que, no que diz respeito ao chamador, ele possa continuar chamando next() e ele continuará retornando valores - como se o método gerador estivesse pausado.Agora, obviamente, você não pode realmente "pausar" um método, então o compilador constrói uma máquina de estado para você lembrar onde está atualmente e como são as variáveis locais, etc.Isso é muito mais fácil do que escrever você mesmo um iterador.
Há um tipo de resposta que acho que ainda não foi dada, entre as muitas respostas excelentes que descrevem como usar geradores.Aqui está a resposta da teoria da linguagem de programação:
O yield
instrução em Python retorna um gerador.Um gerador em Python é uma função que retorna continuações (e especificamente um tipo de corrotina, 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 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 é simples:é o estado de uma computação que ainda não foi concluída.Neste estado, são salvos os valores atuais das variáveis, as operações que ainda não foram executadas, e assim por diante.Então, em algum ponto posterior do programa, a continuação pode ser invocada, de modo que as variáveis do programa sejam redefinidas para esse estado e as operações que foram salvas sejam executadas.
As continuações, nesta forma mais geral, podem ser implementadas de duas maneiras.No call/cc
Dessa forma, a pilha do programa é literalmente salva e, quando a continuação é invocada, a pilha é restaurada.
No estilo de passagem de continuação (CPS), as continuações são apenas funções normais (apenas em linguagens onde as funções são de primeira classe) que o programador gerencia explicitamente e passa para as sub-rotinas.Neste estilo, o estado do programa é representado por encerramentos (e pelas variáveis que estão codificadas neles) em vez de variáveis que residem em algum lugar da pilha.Funções que gerenciam o fluxo de controle aceitam continuação como argumentos (em algumas variações do CPS, as funções podem aceitar múltiplas continuações) e manipulam o fluxo de controle invocando-os simplesmente chamando-os e retornando depois.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 escrever o arquivo em uma continuação (que pode ser potencialmente uma operação muito complexa com muitos detalhes para escrever) e então passa essa continuação (ou seja, como um primeiro passo). fechamento de classe) para outro operador que faz mais processamento e o chama se necessário.(Eu uso muito esse padrão de design na programação de GUI real, seja porque ele economiza linhas de código ou, mais importante, para gerenciar o fluxo de controle após o acionamento de eventos de GUI.)
O restante 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.Os geradores são um subtipo específico de continuação.Enquanto continuações são capazes em geral de salvar o estado de um computação (ou seja, a pilha de chamadas do programa), geradores só são capazes de salvar o estado da iteração durante um iterador.Embora esta 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 provavelmente não é 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 a potência dos geradores:se algo for um iterador, um gerador poderá salvar o estado de sua iteração.
Reiterar:As continuações podem salvar o estado da pilha de um programa e os geradores podem salvar o estado da iteração.Isso significa que as continuações são muito mais poderosas que os geradores, mas também que os geradores são muito, muito mais fáceis.Eles são mais fáceis para o designer da linguagem implementar e são mais fáceis para o programador usar (se você tiver algum tempo livre, tente ler e entender esta página sobre continuações e call/cc).
Mas você poderia facilmente implementar (e conceituar) geradores como um caso simples e específico de estilo de passagem de continuação:
Em qualquer momento yield
é chamado, ele diz à função para retornar uma continuação.Quando a função é chamada novamente, ela começa de onde parou.Portanto, em pseudo-pseudocódigo (ou seja, não pseudocódigo, mas não código), o gerador next
método é 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 o yield
palavra-chave é na verdade um açúcar sintático para a função geradora real, basicamente algo como:
def generatorfun(iterable):
if len(iterable) == 0:
raise StopIteration
else:
return (iterable[0], lambda:generatorfun(iterable[1:]))
Lembre-se de que isso é apenas pseudocódigo e a implementação real dos 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 o yield
palavra-chave.
Aqui está um exemplo em linguagem simples. Fornecerei uma correspondência entre conceitos humanos de alto nível para conceitos de baixo nível Python.
Quero operar em uma sequência de números, mas não quero me incomodar com a criação dessa sequência, quero apenas me concentrar na operação que quero fazer. Então, eu faço o seguinte:
- Eu te chamo e digo que quero uma sequência de números que são produzidos de uma maneira específica, e eu informei qual é o algoritmo.
Esta etapa corresponde adef
Inando a função do gerador, ou seja, a função contendo umyield
. - Algum tempo depois, eu digo: "Ok, prepare -se para me dizer a sequência de números".
Esta etapa corresponde a chamar a função do gerador que retorna um objeto gerador. Observe que você ainda não me diz nenhum número; Você apenas pega seu papel e lápis. - Eu pergunto: "Conte -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. É seu trabalho lembrar onde você estava, quais números você já disse e qual é o próximo número. Eu não me importo com os detalhes.
Esta etapa corresponde à chamada.next()
no objeto gerador. - … Repita a etapa anterior, até…
- Eventualmente, você pode chegar ao fim. Você não me diz um número; Você apenas grita: "Segure seus cavalos! Estou pronto! Não há mais números!"
Esta etapa corresponde ao objeto do gerador que encerrou seu trabalho e levantando umStopIteration
exceção A função do gerador não precisa aumentar a exceção. É aumentado automaticamente quando a função termina ou emite umreturn
.
É isso que um gerador faz (uma função que contém um yield
); começa a executar, faz uma pausa sempre que faz um yield
, e quando solicitado um .next()
Valor continua a partir do ponto em que foi o último. Ele se encaixa perfeitamente por design com o Protocolo Iterador do Python, que descreve como solicitar sequencialmente valores.
O usuário mais famoso do protocolo Iterator é o for
comando em python. Então, sempre que você faz:
for item in sequence:
Não importa se sequence
é uma lista, uma string, um dicionário ou um gerador objeto como descrito acima; O resultado é o mesmo: você lê itens de uma sequência um por um.
Observe que def
inging uma função que contém um yield
A palavra -chave não é a única maneira de criar um gerador; É apenas a maneira mais fácil de criar uma.
Para informações mais precisas, leia sobre tipos de iterador, a declaração de rendimento e geradores na documentação do Python.
Enquanto muitas respostas mostram por que você usaria um yield
Para criar um gerador, há mais usos para yield
. É muito fácil fazer uma coroutina, o que permite a passagem das informações entre dois blocos de código. Não vou repetir nenhum dos bons exemplos que já foram dados sobre o uso yield
Para criar um gerador.
Para ajudar a entender o que um yield
faz no código a seguir, você pode usar o dedo para rastrear o ciclo através de qualquer código que tenha um yield
. Cada vez que seu dedo atinge o yield
, você tem que esperar por um next
ou a send
a ser inserido. Quando um next
é chamado, você rastreia através do código até atingir o yield
… O código à direita do yield
é avaliado e devolvido ao chamador ... então você espera. Quando next
é chamado novamente, você executa outro loop através do código. No entanto, você observará que em uma coroutina, yield
também pode ser usado com um send
… Que enviará um valor do chamador em a função de rendimento. Se um send
é dado, então yield
recebe o valor enviado e o cospe pelo lado esquerdo ... então o traço através do código progride até que você atinja o yield
novamente (retornando o valor no final, como se se next
foi 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()
Tem outro yield
Uso e significado (desde Python 3.3):
yield from <expr>
A partir de PEP 380 - Sintaxe para delegar a um subgenerador:
Uma sintaxe é proposta para um gerador delegar parte de suas operações a outro gerador. Isso permite que uma seção do código que contém 'rendimento' seja fatorada e colocada em outro gerador. Além disso, o subgenerador pode retornar com um valor e o valor é disponibilizado ao gerador delegado.
A nova sintaxe também abre algumas oportunidades de otimização quando um gerador re-re-concorda os valores produzidos por outro.
Além disso isto Introduzirá (desde o Python 3.5):
async def new_coroutine(data):
...
await blocking_action()
Para evitar coroutinas sendo confundidas com um gerador comum (hoje yield
é usado em ambos).
Todas as ótimas respostas, porém um pouco difíceis para iniciantes.
Presumo que você tenha aprendido o return
declaração.
Como uma analogia, return
e yield
são gêmeos. return
significa 'retorno e pare' enquanto 'rendimento' significa 'retorno, mas continue'
- Tente obter um num_list com
return
.
def num_list(n):
for i in range(n):
return i
Executá-lo:
In [5]: num_list(3)
Out[5]: 0
Veja, você obtém apenas um único número em vez de uma lista deles. return
Nunca permite que você prevalece felizmente, apenas implementa uma vez e sai.
- Lá vem
yield
Substituir return
com 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 à return
que corre uma vez e para, yield
Executa vezes que você planeja. Você pode interpretar return
Como return one of them
, e yield
Como return all of them
. Isso é chamado iterable
.
- Mais um passo, podemos reescrever
yield
declaraçã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 lista return
Saídas e o objeto yield
A saída é:
Você sempre obterá [0, 1, 2] de um objeto de lista, mas só poderia recuperá -los do 'objeto yield
saída 'uma vez. Então, ele tem um novo nome generator
objeto conforme exibido em Out[11]: <generator object num_list at 0x10327c990>
.
Em conclusão, como uma metáfora para Grok It:
return
eyield
são gêmeoslist
egenerator
são gêmeos
Aqui estão alguns exemplos Python de como realmente implementar geradores como se Python não tivesse fornecido açúcar sintático para eles:
Como gerador de 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 fechamento de objetos em vez de geradores (Porque ClosuresandObjectSeeeQuivalent)
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)
Eu ia publicar "Read Página 19 do 'Python: Reference essencial' de Beazley para uma descrição rápida dos geradores", mas muitos outros já publicaram boas descrições.
Além disso, observe que yield
pode ser usado em coroutinas como o dual de seu uso nas funções do gerador. Embora não seja o mesmo uso que o seu snippet de código, (yield)
pode ser usado como expressão em uma função. Quando um chamador envia um valor para o método usando o send()
método, então a coroutina será executada até o próximo (yield)
declaração é encontrada.
Geradores e coroutinas são uma maneira legal de configurar aplicativos do tipo fluxo de dados. Eu pensei que valeria a pena saber sobre o outro uso do yield
declaração em funções.
Do ponto de vista de programação, os iteradores são implementados como Thunks.
Para implementar iteradores, geradores e pools de threads para execução simultânea, etc. Como Thunks (também chamados de funções anônimas), usa -se mensagens enviadas para um objeto de fechamento, que possui um despachante, e o despachante responde a "mensagens".
http://en.wikipedia.org/wiki/Message_Passing
"próximo"é uma mensagem enviada para um fechamento, criado pelo"iter" ligar.
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 rendimento.
Aqui está uma demonstração que usa a estrutura dos R6Rs, mas a semântica é absolutamente idêntica à de Python. É o mesmo modelo de computação, e apenas uma mudança na sintaxe é necessária para reescrevê -la no Python.
Welcome to Racket v6.5.0.3. -> (define gen (lambda (l) (define yield (lambda () (if (null? l) 'END (let ((v (car l))) (set! l (cdr l)) v)))) (lambda(m) (case m ('yield (yield)) ('init (lambda (data) (set! l data) 'OK)))))) -> (define stream (gen '(1 2 3))) -> (stream 'yield) 1 -> (stream 'yield) 2 -> (stream 'yield) 3 -> (stream 'yield) 'END -> ((stream 'init) '(a b)) 'OK -> (stream 'yield) 'a -> (stream 'yield) 'b -> (stream 'yield) 'END -> (stream 'yield) 'END ->
Aqui está um exemplo simples:
def isPrimeNumber(n):
print "isPrimeNumber({}) call".format(n)
if n==1:
return False
for x in range(2,n):
if n % x == 0:
return False
return True
def primes (n=1):
while(True):
print "loop step ---------------- {}".format(n)
if isPrimeNumber(n): yield n
n += 1
for n in primes():
if n> 10:break
print "wiriting result {}".format(n)
Resultado:
loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call
Eu não sou um desenvolvedor de Python, mas me parece yield
mantém a posição do fluxo do programa e o próximo loop inicia a partir da posição de "rendimento". 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
Aqui está uma imagem mental do que yield
faz.
Gosto de pensar que um thread tem uma pilha (mesmo quando não é implementado dessa forma).
Quando uma função normal é chamada, ela coloca suas variáveis locais na pilha, faz alguns cálculos, depois limpa a pilha e retorna.Os valores de suas variáveis locais nunca mais serão vistos.
Com um yield
função, quando seu código começa a ser executado (ou seja,depois que a função é chamada, retornando um objeto gerador, cujo next()
método é então invocado), ele coloca suas variáveis locais na pilha e calcula por um tempo.Mas então, quando atinge o yield
instrução, antes de limpar sua parte da pilha e retornar, ela tira um instantâneo de suas variáveis locais e as armazena no objeto gerador.Ele também anota o local onde está atualmente em seu código (ou seja,o particular yield
declaração).
Portanto, é uma espécie de função congelada na qual o gerador está preso.
Quando next()
é chamado posteriormente, ele recupera os pertences da função na pilha e a reanima.A função continua a calcular de onde parou, alheia ao fato de que acabou de passar uma eternidade em um armazenamento refrigerado.
Compare os seguintes exemplos:
def normalFunction():
return
if False:
pass
def yielderFunction():
return
if False:
yield 12
Quando chamamos a segunda função, ela se comporta de maneira muito diferente da primeira.O yield
declaraçã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>
Chamando yielderFunction()
não executa seu código, mas transforma o código em um gerador.(Talvez seja uma boa ideia nomear essas coisas com o yielder
prefixo para 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']
O gi_code
e gi_frame
campos são onde o estado congelado é armazenado.Explorando-os com dir(..)
, podemos confirmar que nosso modelo mental acima é confiável.
Como todas as respostas sugerem, yield
é usado para criar um gerador de sequência. É usado para gerar alguma sequência dinamicamente. Por exemplo, ao ler uma linha de arquivo por linha em uma rede, você pode usar o yield
função da seguinte forma:
def getNextLines():
while con.isOpen():
yield con.read()
Você pode usá -lo no seu código da seguinte forma:
for line in getNextLines():
doSomeThing(line)
Transferência de controle de execução Gotcha
O controle de execução será transferido de getNextLines () para o for
loop quando o rendimento é executado. Assim, toda vez que GetNextLines () é invocado, a execução começa a partir do ponto em que foi interrompida 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
irá imprimir
"first time"
"second time"
"third time"
"Now some useful value 12"
Rendimento é um objeto
UMA return
em uma função retornará um único valor.
Se você quiser uma função para retornar um enorme conjunto de valores, usar yield
.
Mais importante, yield
é um barreira.
Como a barreira no idioma CUDA, ela não transferirá o controle até que seja concluído.
Isto é, ele executará o código em sua função desde o início até que atinja yield
. Em seguida, ele retornará o primeiro valor do loop.
Em seguida, todas as outras chamadas executarão o loop que você escreveu na função mais uma vez, retornando o próximo valor até que não haja nenhum valor para retornar.
(Minha resposta abaixo fala apenas da perspectiva de usar o gerador python, não o Implementação subjacente do mecanismo do gerador, que envolve alguns truques de manipulação de pilha e heap.)
Quando yield
é usado em vez de um return
Em uma função python, essa função é transformada em algo especial chamado generator function
. Essa função retornará um objeto de generator
modelo. o yield
A palavra -chave é um sinalizador para notificar o compilador Python para tratar essa função especialmente. As funções normais terão rescindir assim que algum valor for retornado. Mas com a ajuda do compilador, a função do gerador pode ser pensado em como resumível. Ou seja, o contexto de execução será restaurado e a execução continuará a partir da última execução. Até você ligar explicitamente retorno, o que aumentará um StopIteration
Exceção (que também faz parte do protocolo do iterador) ou chega ao final da função. Eu encontrei muitas referências sobre generator
mas isso 1 de functional programming perspective
é o mais digestível.
(Agora eu quero falar sobre a lógica por trás generator
, e as iterator
baseado em meu próprio entendimento. Espero que isso possa ajudá -lo a entender o motivação essencial de iterador e gerador. Esse conceito aparece em outros idiomas, como C#.)
Pelo que entendi, quando queremos processar um monte de dados, geralmente armazenamos os dados em algum lugar e depois o processamos um por um. Mas isso ingénuo A abordagem é problemática. Se o volume de dados for enorme, é caro armazená -los como um todo antes. Então, em vez de armazenar o data
diretamente, por que não armazenar algum tipo de metadata
Indiretamente, ou seja the logic how the data is computed
.
Existem 2 abordagens para envolver esses metadados.
- A abordagem OO, embrulhamos os metadados
as a class
. Este é o chamadoiterator
quem implementa o protocolo do iterador (ou seja, o__next__()
, e__iter__()
métodos). Este também é o comumente visto Padrão de design do iterador. - A abordagem funcional, embrulhamos os metadados
as a function
. Este é o chamadogenerator function
. Mas sob o capô, o devolvidogenerator object
aindaIS-A
Iterador porque também implementa o protocolo do iterador.
De qualquer forma, um iterador é criado, ou seja, algum objeto que pode fornecer os dados que você deseja. A abordagem OO pode ser um pouco complexa. De qualquer forma, qual deles usar depende de você.
Em resumo, o yield
a declaração transforma sua função em uma fábrica que produz um objeto especial chamado generator
que envolve o corpo da sua função original. Quando o generator
é iterado, ele executa sua função até atingir o próximo yield
Em seguida, suspende a execução e avalia o valor passado para yield
. Ele repete esse processo em cada iteração até que o caminho da execução sai 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
O poder vem do uso do gerador com um loop que calcula uma sequência, o gerador executa o loop para que a cada vez "produza" o próximo resultado do cálculo, dessa maneira calcula uma lista em tempo real, sendo o benefício a memória salvo para cálculos especialmente grandes
Diga que você queria criar um seu próprio range
função que produz uma gama iterável de números, você pode fazer assim,
def myRangeNaive(i):
n = 0
range = []
while n < i:
range.append(n)
n = n + 1
return range
e use assim;
for i in myRangeNaive(10):
print i
Mas isso é ineficiente porque
- Você cria uma matriz que você usa apenas uma vez (isso desperdiça a memória)
- Esse código realmente atravessa essa matriz 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é atingir uma declaração de 'rendimento' na qual ela para e 'produz' o valor ou atinge o final da função. Neste caso na primeira chamada, next()
Executa até a declaração de rendimento e rendimento 'n', na próxima chamada, ele executará a declaração de incremento, voltará para o 'while', avaliará e, se for verdade, ele parará e cederá 'n' novamente, ele irá Continue assim até que a condição retorne falsa e o gerador salte até o final da função.
Muitas pessoas usam return
ao invés de yield
, mas em alguns casos yield
pode ser mais eficiente e mais fácil de trabalhar.
Aqui está um exemplo que yield
é definitivamente o melhor para:
Retorna (em função)
import random
def return_dates():
dates = [] # With 'return' you need to create a list then return it
for i in range(5):
date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
dates.append(date)
return dates
colheita (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 yield
usa três linhas em vez de cinco e tem uma variável 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()
dá uma lista e yield_dates()
dá um gerador.
Um exemplo da vida real seria algo como ler uma linha por linha ou se você deseja apenas fazer um gerador.
yield
é como um elemento de retorno para uma função. A diferença é que o yield
O elemento transforma uma função em um gerador. Um gerador se comporta como uma função até que algo seja 'rendido'. O gerador para até que seja chamado a seguir e continua exatamente do mesmo ponto que iniciou. Você pode obter uma sequência de todos os valores 'rendidos' em um, ligando list(generator())
.
o yield
A palavra -chave simplesmente coleta os resultados retornados. Imagine yield
Curti return +=
Aqui está um simples yield
Abordagem baseada, para calcular a série Fibonacci, explicou:
def fib(limit=50):
a, b = 0, 1
for i in range(limit):
yield b
a, b = b, a+b
Quando você entra no seu REPL e tenta chamá -lo, você obterá um resultado misterioso:
>>> fib()
<generator object fib at 0x7fa38394e3b8>
Isso ocorre porque a presença de yield
sinalizou para Python que você deseja criar um gerador, isto é, um objeto que gera valores sob demanda.
Então, como você gera esses valores? Isso pode ser feito diretamente usando a função interna next
, ou, indiretamente, alimentando -o com uma construção que consome valores.
Usando o embutido next()
função, 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 fib
para um for
Loop, a list
Inicializador, a tuple
Inicializador, ou qualquer outra coisa que espere um objeto que gera/produz valores, você "consumirá" o gerador até que não mais valores possam ser produzidos por ele (e retorna):
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 tuple
Inicializador:
>>> tuple(fib(5)) # consumes fib
(1, 1, 2, 3, 5)
Um gerador difere de uma função no sentido de que é preguiçoso. Isso faz isso mantendo seu estado local e permitindo que você retome sempre que precisar.
Quando você invoca pela primeira vez fib
chamando -o:
f = fib()
Python compila a função, encontra o yield
palavra -chave e simplesmente retorna um objeto gerador de volta para você. Não é muito útil, parece.
Quando você solicita, gera o primeiro valor, direta ou indiretamente, ele executa todas as declarações que encontra, até que encontre um yield
, então produz de volta o valor que você forneceu yield
e pausas. Para um exemplo que melhor demonstra isso, vamos usar alguns print
chamadas (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!")
Agora você tem um objeto gerador esperando um comando para gerar um valor. Usar next
E 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 cotados são o que é impresso. O resultado citado é o que é devolvido yield
. Ligar next
de novo 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 em yield value
e currículos a partir daí. A próxima mensagem é impressa e a pesquisa para o yield
declaração para pausar no desempenho executado novamente (devido ao while
ciclo).
Um exemplo fácil de entender o que é: yield
def f123():
for _ in range(4):
yield 1
yield 2
for i in f123():
print i
A saída é:
1 2 1 2 1 2 1 2