Generators — pequenos ajustes/tunnings que fazem diferença 1
Estudar por conta, fazer curso, ler livros e artigos são coisas muito boas e nos ajudam muito em relação a conhecimento, mas eu sempre falo que as melhores escolas são o dia a dia e os problemas da vida real.
Porque estou falando isso? Porque estive trabalhando em sistemas críticos no Walmart, VivaReal Tech (agora GrupoZAP) e estou trabalhando também em alguns sistemas em um “freela” (desde 2017) e vi que “detalhes” fazem toda a diferença. Principalmente quando a performance importa e temos que responder o mais rápido possível. Além do mais, uso de CPU e memória custam dinheiro. Um código que não performa bem além de custar tempo (que já é dinheiro por si) tem um custo alto em recursos.
Estamos sempre correndo atrás de como ganhar/otimizar performance da aplicação/sistema.
Este é o primeiro post da série “Pequenos ajustes/tunning que fazem diferença”. E nesse vamos falar de GENERATORS.
💛
Vou utilizar Python 3 para dar os exemplos. As configurações da minha máquina estão no final.
Generators FTW
Existe a possiblidade de criar generator functions e generator expressions.
Exemplo de generator function:
def gen_func():
for i in range(3):
yield i
Exemplo de generator expression:
gen_expr = (i for i in range(3))
Generators foram introduzidos na PEP 255. Generator functions criam generator iterators. WTF? Se você não souber como funcionam iterators, é importante dar uma estudada. :) Mas basicamente é um objeto que você pode iterar um item por vez.
Vamos aos códigos, comparar primeiramente qual a diferença entre o uso e a ausência de generator.
Eu criei um código que cria uma lista e itera 200 milhões de inteiros.
Rodei algumas vezes e peguei o resultado mais rápido:
finish, elapsed time 31.552 seconds
Fazendo a mesma coisa utilizando generator:
Dessa forma, o resultado mais lento que peguei foi:
finish, elapsed time 20.69 seconds
Claramente mais rápido. Tá, mas como funciona? Por conta do lazy evaluation conseguimos trabalhar com os dados sem precisar pegar todo o conjunto de dados e, sim, obtendo um por um. O estado da “função” é “salvo” quando o yield é chamado e, assim, quando pedimos o próximo item do iterator (next) obtemos o valor guardado naquele estado.
Você pode ver na prática. Com a lista:
No output, obtemos:
0 will be added in list
1 will be added in list
2 will be added in list
0 in list
1 in list
2 in list
E utilizando generator:
No output, obtemos:
0 will be yield
0 generated
1 will be yield
1 generated
2 will be yield
2 generated
Bom, existem casos e casos. Na minha opinião, Generators functions são muito úteis quando existem muitos dados para serem processados. Há momentos em que eles não fazem tanta diferença assim.
Detalhes importantes sobre Generators:
- Generator é um tipo especial de iterator.
- Você “economiza” em memória.
- Só pode ser utilizado uma vez.
- Iterators tem lazy evaluation (só retorna o próximo quando “pedimos”).
- É possível usar o next (mesmo que usamos com iterator) para pegar o próximo valor (next(iterator)).
Conclusão
Sempre podemos melhorar nosso código de vários modos. Performance tem sido cada vez mais importante e é sempre legal compartilhar com a comunidade o que você aprendeu.
Em breve postarei algo relacionado à performance também. Será “Go — Channels, routines e memória” e o tradeoff que vem com tudo isso junto.
Configurações da minha máquina
SO: macOS High Sierra
Processor Name: Intel Core i5
Processor Speed: 2,4 GHz
Number of Processors: 1
Total Number of Cores: 2
Memory: 8 GB 1600 MHz DDR3
Refs.