Nota: este post assume a sintaxe do Python 3.x. †
Um gerador é simplesmente uma função que retorna um objeto no qual você pode chamar next
, de modo que, para cada chamada, retorne algum valor, até StopIteration
gerar uma exceção, sinalizando que todos os valores foram gerados. Esse objeto é chamado de iterador .
As funções normais retornam um único valor usando return
, assim como em Java. No Python, no entanto, existe uma alternativa, chamada yield
. Usar yield
qualquer lugar em uma função faz com que seja um gerador. Observe este código:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Como você pode ver, myGen(n)
é uma função que gera n
e n + 1
. Toda chamada paranext
gera um único valor, até que todos os valores tenham sido produzidos. for
loops são chamados next
em segundo plano, assim:
>>> for n in myGen(6):
... print(n)
...
6
7
Da mesma forma, existem expressões geradoras , que fornecem um meio para descrever sucintamente certos tipos comuns de geradores:
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Observe que expressões geradoras são muito parecidas compreensões de lista :
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Observe que um objeto gerador é gerado uma vez , mas seu código não é executado de uma só vez. Apenas chamadas para next
realmente executar (parte do) código. A execução do código em um gerador para quando uma yield
instrução é atingida, na qual retorna um valor. A próxima chamada next
então fará com que a execução continue no estado em que o gerador foi deixado após o último yield
. Essa é uma diferença fundamental nas funções regulares: elas sempre iniciam a execução no "topo" e descartam seu estado ao retornar um valor.
Há mais coisas a serem ditas sobre esse assunto. Por exemplo, é possívelsend
retornar os dados a um gerador ( referência ). Mas isso é algo que eu sugiro que você não analise até entender o conceito básico de um gerador.
Agora você pode perguntar: por que usar geradores? Existem algumas boas razões:
- Certos conceitos podem ser descritos de maneira muito mais sucinta usando geradores.
- Em vez de criar uma função que retorna uma lista de valores, pode-se escrever um gerador que gere os valores em tempo real. Isso significa que nenhuma lista precisa ser construída, o que significa que o código resultante é mais eficiente em termos de memória. Dessa maneira, pode-se até descrever fluxos de dados que seriam simplesmente grandes demais para caber na memória.
Os geradores permitem uma maneira natural de descrever fluxos infinitos . Considere, por exemplo, os números de Fibonacci :
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Esse código usa itertools.islice
para obter um número finito de elementos de um fluxo infinito. É recomendável que você observe bem as funções do itertools
módulo, pois são ferramentas essenciais para escrever geradores avançados com grande facilidade.
† Sobre o Python <= 2.6: nos exemplos acima, next
é uma função que chama o método __next__
no objeto especificado. No Python <= 2.6, utiliza-se uma técnica ligeiramente diferente, a saber, em o.next()
vez de next(o)
. O Python 2.7 foi next()
chamado, .next
então você não precisa usar o seguinte no 2.7:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
send
dados para um gerador. Depois de fazer isso, você tem uma 'corotina'. É muito simples implementar padrões como o Consumidor / Produtor mencionado com corotinas, porque eles não precisam de se,Lock
portanto, não podem entrar em conflito. É difícil descrever corotinas sem contornar threads; portanto, direi apenas que as corotinas são uma alternativa muito elegante ao encadeamento.Um gerador é efetivamente uma função que retorna (dados) antes de terminar, mas faz uma pausa nesse ponto e você pode retomar a função nesse momento.
e assim por diante. O (ou um) benefício dos geradores é que, como eles lidam com dados, um por vez, você pode lidar com grandes quantidades de dados; com listas, requisitos excessivos de memória podem se tornar um problema. Os geradores, assim como as listas, são iteráveis, portanto podem ser usados da mesma maneira:
Observe que os geradores fornecem outra maneira de lidar com o infinito, por exemplo
O gerador encapsula um loop infinito, mas isso não é um problema, porque você só obtém cada resposta toda vez que solicita.
fonte
Primeiro de tudo, o termo gerador originalmente estava um pouco mal definido no Python, levando a muita confusão. Você provavelmente quer dizer iteradores e iterables (veja aqui ). Em Python, também existem funções geradoras (que retornam um objeto gerador), objetos geradores (que são iteradores) e expressões geradoras (que são avaliadas para um objeto gerador).
De acordo com a entrada do glossário para gerador , parece que a terminologia oficial é agora que o gerador é a abreviação de "função de gerador". No passado, a documentação definia os termos de maneira inconsistente, mas felizmente isso foi corrigido.
Ainda pode ser uma boa idéia ser preciso e evitar o termo "gerador" sem especificações adicionais.
fonte
Os geradores podem ser considerados atalhos para a criação de um iterador. Eles se comportam como um Iterator Java. Exemplo:
Espero que isso ajude / é o que você está procurando.
Atualizar:
Como muitas outras respostas estão sendo exibidas, existem diferentes maneiras de criar um gerador. Você pode usar a sintaxe entre parênteses, como no meu exemplo acima, ou usar yield. Outra característica interessante é que os geradores podem ser "infinitos" - iteradores que não param:
fonte
Stream
s, que são muito mais parecidos com geradores, exceto que você aparentemente não pode obter o próximo elemento sem uma quantidade surpreendente de problemas.Não há equivalente em Java.
Aqui está um exemplo artificial:
Há um loop no gerador que roda de 0 a n e, se a variável do loop for um múltiplo de 3, ela produzirá a variável.
Durante cada iteração do
for
loop, o gerador é executado. Se é a primeira vez que o gerador é executado, ele inicia no início, caso contrário, continua a partir do tempo anterior que produziu.fonte
print "hello"
após ox=x+1
exemplo, "hello" seria impresso 100 vezes, enquanto o corpo do loop for ainda seria executado apenas 33 vezes.Eu gosto de descrever geradores, para aqueles com uma experiência decente em linguagens de programação e computação, em termos de quadros de pilha.
Em muitos idiomas, há uma pilha em cima da qual está o "quadro" atual da pilha. O quadro da pilha inclui espaço alocado para variáveis locais para a função, incluindo os argumentos passados para essa função.
Quando você chama uma função, o ponto de execução atual (o "contador de programa" ou equivalente) é empurrado para a pilha e um novo quadro de pilha é criado. A execução é transferida para o início da função que está sendo chamada.
Com funções regulares, em algum momento a função retorna um valor e a pilha é "popped". O quadro de pilha da função é descartado e a execução é retomada no local anterior.
Quando uma função é um gerador, ela pode retornar um valor sem que o quadro da pilha seja descartado, usando a instrução yield. Os valores das variáveis locais e o contador do programa dentro da função são preservados. Isso permite que o gerador seja reiniciado posteriormente, com a execução continuando a partir da declaração de rendimento, e pode executar mais código e retornar outro valor.
Antes do Python 2.5, todos os geradores faziam isso. Python 2.5 adicionado a capacidade para passar valores de volta em que o gerador bem. Ao fazer isso, o valor passado está disponível como uma expressão resultante da declaração de rendimento que retornou temporariamente o controle (e um valor) do gerador.
A principal vantagem para os geradores é que o "estado" da função é preservado, diferentemente das funções regulares em que toda vez que o quadro da pilha é descartado, você perde todo esse "estado". Uma vantagem secundária é que algumas das despesas gerais da chamada de função (criação e exclusão de quadros de pilha) são evitadas, embora essa seja geralmente uma vantagem menor.
fonte
A única coisa que posso acrescentar à resposta de Stephan202 é a recomendação de que você dê uma olhada na apresentação PyCon '08 de David Beazley "Truques de geradores para programadores de sistemas", que é a melhor explicação individual de como e por que os geradores que eu já vi qualquer lugar. Foi isso que me levou de "Python parece meio divertido" a "É isso que eu estava procurando". Está em http://www.dabeaz.com/generators/ .
fonte
Ajuda a fazer uma distinção clara entre a função foo e o gerador foo (n):
foo é uma função. foo (6) é um objeto gerador.
A maneira típica de usar um objeto gerador é em um loop:
O loop imprime
Pense em um gerador como uma função recuperável.
yield
se comporta comoreturn
no sentido de que os valores gerados são "retornados" pelo gerador. Ao contrário do retorno, no entanto, na próxima vez em que o gerador for solicitado a fornecer um valor, a função do gerador, foo, retoma de onde parou - após a última declaração de rendimento - e continua a funcionar até atingir outra declaração de rendimento.Nos bastidores, quando você chama
bar=foo(6)
a barra de objetos do gerador é definida para você ter umnext
atributo.Você pode chamá-lo para recuperar valores gerados por foo:
Quando foo termina (e não há mais valores produzidos), a chamada
next(bar)
gera um erro StopInteration.fonte
Este post usará os números de Fibonacci como uma ferramenta para explicar a utilidade dos geradores Python .
Esta postagem apresentará códigos C ++ e Python.
Os números de Fibonacci são definidos como a sequência: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....
Ou em geral:
Isso pode ser transferido para uma função C ++ com extrema facilidade:
Mas se você quiser imprimir os seis primeiros números de Fibonacci, recalculará muitos valores com a função acima.
Por exemplo :,
Fib(3) = Fib(2) + Fib(1)
masFib(2)
também recalculaFib(1)
. Quanto maior o valor que você deseja calcular, pior será.Portanto, pode-se tentar reescrever o que foi dito acima, acompanhando o estado em
main
.Mas isso é muito feio e complica nossa lógica
main
. Seria melhor não ter que se preocupar com o estado em nossamain
função.Poderíamos retornar um
vector
de valores e usar umiterator
para iterar sobre esse conjunto de valores, mas isso requer muita memória ao mesmo tempo para um grande número de valores de retorno.Voltando à nossa antiga abordagem, o que acontece se quisermos fazer outra coisa além de imprimir os números? Teríamos que copiar e colar todo o bloco de código
main
e alterar as instruções de saída para o que mais desejássemos fazer. E se você copiar e colar o código, deverá levar um tiro. Você não quer levar um tiro, não é?Para resolver esses problemas e evitar ser atingido, podemos reescrever esse bloco de código usando uma função de retorno de chamada. Sempre que um novo número de Fibonacci é encontrado, chamaríamos a função de retorno de chamada.
Isso é claramente uma melhoria, sua lógica
main
não é tão confusa e você pode fazer o que quiser com os números de Fibonacci, basta definir novos retornos de chamada.Mas isso ainda não é perfeito. E se você quisesse obter apenas os dois primeiros números de Fibonacci e, em seguida, fazer alguma coisa, obter mais um pouco e fazer outra coisa?
Bem, poderíamos continuar como estivemos, e poderíamos começar a adicionar novamente o estado
main
, permitindo que GetFibNumbers iniciasse de um ponto arbitrário. Mas isso vai inchar ainda mais o nosso código, e já parece grande demais para uma tarefa simples como imprimir números de Fibonacci.Poderíamos implementar um modelo de produtor e consumidor por meio de alguns threads. Mas isso complica ainda mais o código.
Em vez disso, vamos falar sobre geradores.
O Python possui um recurso de linguagem muito agradável que resolve problemas como esses chamados geradores.
Um gerador permite que você execute uma função, pare em um ponto arbitrário e continue novamente de onde parou. Sempre que retornar um valor.
Considere o seguinte código que usa um gerador:
O que nos dá os resultados:
A
yield
declaração é usada em conjunto com geradores Python. Ele salva o estado da função e retorna o valor gerado. Na próxima vez que você chamar a função next () no gerador, ela continuará de onde o rendimento parou.Isso é muito mais limpo que o código da função de retorno de chamada. Temos código mais limpo, menor e sem mencionar muito mais código funcional (o Python permite números inteiros arbitrariamente grandes).
Fonte
fonte
Acredito que a primeira aparição de iteradores e geradores ocorreu na linguagem de programação Icon, há cerca de 20 anos.
Você pode apreciar a visão geral do Icon , que permite envolvê-los sem se concentrar na sintaxe (já que Icon é um idioma que você provavelmente não conhece, e Griswold estava explicando os benefícios desse idioma para pessoas provenientes de outros idiomas).
Depois de ler apenas alguns parágrafos, a utilidade de geradores e iteradores pode se tornar mais aparente.
fonte
A experiência com a compreensão de listas mostrou sua ampla utilidade em todo o Python. No entanto, muitos dos casos de uso não precisam ter uma lista completa criada na memória. Em vez disso, eles só precisam iterar sobre os elementos um de cada vez.
Por exemplo, o seguinte código de soma criará uma lista completa de quadrados na memória, iterará sobre esses valores e, quando a referência não for mais necessária, excluirá a lista:
sum([x*x for x in range(10)])
A memória é conservada usando uma expressão de gerador:
sum(x*x for x in range(10))
Benefícios semelhantes são conferidos aos construtores para objetos de contêiner:
Expressões de gerador são especialmente úteis com funções como sum (), min () e max () que reduzem uma entrada iterável para um único valor:
Mais
fonte
Eu coloquei este pedaço de código que explica 3 conceitos-chave sobre geradores:
fonte