Generators — pequenos ajustes/tunnings que fazem diferença 1

Caleb Woods on Unsplash

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.