Os generators são a forma como a linguagem de programação Python permite criar iteráveis através de uma função.

Uma função normal retorna um valor com o uso da palavra return. Um generator é tal como uma função, mas em vez de usar a palavra return usa a palavra yield uma ou mais vezes (pode também usar a palavra return para sair da função). Ao chamar a função generator é criado um iterável, e ao iterar sobre o mesmo é executado o código do generetor. Cada vez que um yield é executado ele produz um novo valor no stream do iterável.

def ola_mundo():
    yield "Ola"
    yield "mundo"

for x in ola_mundo():
    print x


Ola
mundo



Este pequeno exemplo ilustrativo de um generator, contém apenas duas chamadas da expressão yield. Ao iterarmos sobre a função ola_mundo() com um ciclo for verificamos que este generator produz uma stream de dois valores, "Ola" e "mundo".

Os generators normalmente não são assim tão simples, tipicamente o código de um generator terá um ciclo for com chamadas à expressão yield.

def numeros_pares(stream):
    for n in stream:
        if n % 2 == 0:
            yield n

for n in numeros_pares(nums):
    faz_algo(n)

Podemos reescrever a função numeros_pares() como um generator. O código é praticamente o mesmo, mas em vez de produzir uma lista e devolvê-la no fim da função, simplesmente produz os valores assim que os encontra.

Chamamos esta função numeros_pares() da mesma forma, passando-lhe o iterável que queremos que seja trabalhado e a função devolve-nos um iterador que produzirá os valores pares desse iterável.

A função anterior de numeros_pares() construía uma lista de números pares que nos devolvia no fim, isso significa que nada mais acontecia no nosso programa enquanto a função não examinasse a sequência completa do iterável e seleccionasse os valores pares para a nova lista. Para um iterável pequeno isso poderá não ser um problema mas será certamente para um iterável grande.

Já a versão generator da função devolverá o primeiro número par que encontrar deixando o nosso programa disponível para prosseguir com o seu trabalho antes que toda a sequência do iterável seja examinada. Isto significa que a função pode trabalhar indefinidamente ou mesmo com streams infinitas.

Preguiça


Os generators têm a vantagem de serem "preguiçosos", eles não "trabalham" até que o primeiro valor lhe seja pedido, e quando isso acontece limitam-se a dar esse valor e voltam a parar, não voltando a "trabalhar" até que o próximo valor seja pedido. Isto faz com que os generators usem poucos recursos e sejam mais indicados para certos tipos de iteráveis.

Os generators podem ser inicialmente confusos, eles são muito semelhantes a funções mas têm diferenças importantes. Primeiro chamar um generator não faz com que o seu código seja imediatamente executado, ele simplesmente cria um iterável. Só quando um ciclo for ou outro "consumidor" do iterável começar a pedir valores do stream é que o código do generator começa a ser executado.

Quando a expressão yield é encontrada, a execução é suspensa, e o valor é usado como o próximo valor da stream. Numa função normal a expressão return termina a função. Mas num generator o estado actual é memorizado e quando o próximo valor for solicitado, a execução do generator continua onde tinha ficado anteriormente, depois da expressão yield. O próximo valor da stream é produzido quando uma expressão yield é novamente executada. Todas as variáveis locais do generator mantêm os seus valores durante todo o percurso do stream. Isto torna os generators muito convenientes para escrever iteráveis.

O iterador termina quando o generator retorna, quer explicitamente com a expressão return, ou quando executa a última expressão do código do generator.




A série de posts sobre ciclos em Python tem continuação nos próximos capítulos.


Este post faz parte da série de posts sobre Ciclos e Interáveis em Python:
  1. Ciclos em Python, o básico
  2. Ciclos em Python e os Iteráveis
  3. Ciclos em Python, mais exemplos de Iteráveis
  4. Ciclos em Python, uso de Iteráveis fora dos ciclos
  5. Ciclos em Python, problemas comuns e os índices
  6. Ciclos em Python, iterar sobre duas listas
  7. Ciclos em Python, iteração personalizada
  8. Ciclos em Python, Generators - parte 1 (post actual)
  9. Ciclos em Python, Generators - parte 2
  10. Ciclos em Python, Generators - parte 3
  11. Ciclos em Python, operações de baixo nível
  12. Ciclos em Python, como tornar os nossos objectos em Iteráveis
  13. Ciclos em Python, conclusão


A inspiração para este post veio daqui.