Os
generators são uma forma de 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 generatores 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.
Os generatores 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 generatores 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.
A inspiração para este post veio
daqui.
"
Ciclos em Python, Generators - parte 1" é um post originalmente publicado no blog
sergiosilvablog.com