Entendendo geradores em Python

218

Atualmente, estou lendo o livro de receitas Python e atualmente estou olhando para geradores. Estou achando difícil de entender.

Como eu venho de um background em Java, existe um equivalente em Java? O livro estava falando sobre 'Produtor / Consumidor', no entanto, quando ouço isso, penso em encadear.

O que é um gerador e por que você o usaria? Sem citar nenhum livro, obviamente (a menos que você possa encontrar uma resposta decente e simplista diretamente de um livro). Talvez com exemplos, se você estiver se sentindo generoso!

Federer
fonte

Respostas:

402

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é StopIterationgerar 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 yieldqualquer 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 ne n + 1. Toda chamada paranext gera um único valor, até que todos os valores tenham sido produzidos. forloops são chamados nextem 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 nextrealmente executar (parte do) código. A execução do código em um gerador para quando uma yieldinstrução é atingida, na qual retorna um valor. A próxima chamada nextentã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.islicepara obter um número finito de elementos de um fluxo infinito. É recomendável que você observe bem as funções do itertoolsmó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, .nextentão você não precisa usar o seguinte no 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
Stephan202
fonte
9
Você mencionou que é possível fazer senddados 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, Lockportanto, 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.
Jochen Ritzel
Os geradores Python são basicamente máquinas de Turing em termos de como eles funcionam?
Fiery Phoenix
48

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.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

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:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Observe que os geradores fornecem outra maneira de lidar com o infinito, por exemplo

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

O gerador encapsula um loop infinito, mas isso não é um problema, porque você só obtém cada resposta toda vez que solicita.

Caleb Hattingh
fonte
30

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.

Nikow
fonte
2
Hmm, acho que você está certo, pelo menos de acordo com um teste de algumas linhas no Python 2.6. Uma expressão de gerador retorna um iterador (também conhecido como 'objeto gerador'), não um gerador.
Craig McQueen
22

Os geradores podem ser considerados atalhos para a criação de um iterador. Eles se comportam como um Iterator Java. Exemplo:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
pensar demais
fonte
1
Agora, Java tem Streams, 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.
Fund Monica's Lawsuit
12

Não há equivalente em Java.

Aqui está um exemplo artificial:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

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

Wernsey
fonte
2
O último parágrafo é muito importante: O estado da função do gerador é 'congelado' toda vez que produz sth e continua exatamente no mesmo estado quando é invocado na próxima vez.
Johannes Charra 18/11/2009
Não há equivalente sintático em Java para uma "expressão de gerador", mas os geradores - assim que você tiver uma - são essencialmente apenas um iterador (mesmas características básicas de um iterador Java).
18720 Overthink
@overthink: Bem, geradores podem ter outros efeitos colaterais que os iteradores Java não podem ter. Se eu colocasse print "hello"após o x=x+1exemplo, "hello" seria impresso 100 vezes, enquanto o corpo do loop for ainda seria executado apenas 33 vezes.
Wernsey
@ iWerner: Certeza de que o mesmo efeito possa ter no Java. A implementação de next () no iterador Java equivalente ainda teria que procurar de 0 a 99 (usando o exemplo mygen (100)), para que você pudesse System.out.println () sempre que quisesse. Você retornaria apenas 33 vezes a partir do próximo (). O que falta a Java é a sintaxe de rendimento muito útil, que é significativamente mais fácil de ler (e escrever).
22420 Overthink
Adorei ler e lembrar-me desta definição de uma linha: se é a primeira vez que o gerador é executado, ele inicia no início, caso contrário, continua a partir do tempo anterior que produziu.
Iqra.
8

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.

Peter Hansen
fonte
6

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

Robert Rossney
fonte
6

Ajuda a fazer uma distinção clara entre a função foo e o gerador foo (n):

def foo(n):
    yield n
    yield n+1

foo é uma função. foo (6) é um objeto gerador.

A maneira típica de usar um objeto gerador é em um loop:

for n in foo(6):
    print(n)

O loop imprime

# 6
# 7

Pense em um gerador como uma função recuperável.

yieldse comporta como returnno 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 um nextatributo.

Você pode chamá-lo para recuperar valores gerados por foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Quando foo termina (e não há mais valores produzidos), a chamada next(bar)gera um erro StopInteration.

unutbu
fonte
5

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:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Isso pode ser transferido para uma função C ++ com extrema facilidade:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

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)mas Fib(2)também recalcula Fib(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.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

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 vectorde valores e usar um iteratorpara 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 maine 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.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Isso é claramente uma melhoria, sua lógica mainnã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:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

O que nos dá os resultados:

0 1 1 2 3 5

A yielddeclaraçã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

Brian R. Bondy
fonte
3

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.

Nosredna
fonte
2

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:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

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:

max(len(line)  for line in file  if line.strip())

Mais

Saqib Mujtaba
fonte
1

Eu coloquei este pedaço de código que explica 3 conceitos-chave sobre geradores:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Stefan Iancu
fonte