Como usar Generators em Python — pt. 1

Victor Turrisi
4 min readSep 8, 2020

--

E ai galera,

Acho que existe uma grande demanda por conteúdo de Python em Português e por isso decidi começar a escrever sobre o assunto. A ideia geral é tratar de tópicos em Python com exemplos e de forma direta, sem perder a profundidade do assunto.

Só para me apresentar, meu nome é Victor, tenho um mestrado e bacharelado em Ciência da Computação pela UEL e atualmente estou no doutorado na UniTN na Itália. Trabalho com Python desde 2014 e é praticamente a linguagem exclusiva (com algumas exceções) que eu uso diariamente.

Vamos ao que interessa: Generators.

Se você já mexeu com Python, você já usou algum generator, mesmo sem saber (a menos que você seja uma das pessoas que usa Python 2 sem propósito algum e, se você é, por quê?!).

range(): todo mundo usa, mas você sabia que ele é um generator?

Generators (e, consequentemente, Iterators) são uma classe (no sentido de conjunto e não uma “class”) de objetos que implementam alguns métodos “especiais”, ou mágicos, ou dunders, que tornam possível iterar sobre esse objeto. Não vou entrar a fundo em como esses métodos mágicos funcionam (e sim, esse é o nome) mas pretendo fazer alguma postagem disso no futuro.

Vamos considerar um iterator qualquer:

# um exemplo de um obj iterável (não é um generator)
l = [1, 2, 3]
for i in l:
print(i)
# 1
# 2
# 3

Um generator funciona da mesma maneira. Você pode usar com for, utilizar para list comprehensions, ou qualquer outra situação que você precise de algo iterável. Entretanto, diferentemente de objetos iteráveis tradicionais, um generator é lazy e não tem a capacidade de armazenar todos os elementos de uma sequência.

Um generator pode ser interpretado da seguinte maneira: armazena o elemento atual da sequência, a operação que deve ser feita para se conseguir o próximo elemento e um critério de parada. O que acontece, na realidade, é que ele salva a posição de onde o código parou a execução (contexto), retorna algum valor e, quando requisitado, retoma a execução do mesmo local.

Ou seja, a maior vantagem de um generator em relação a um iterator tradicional é que todos os elementos da sequência não precisam ser armazenados ao mesmo tempo = menos consumo de memória de “graça”.

O exemplo mais conhecido:

for i in range(3):
print(i)

Diferente do exemplo anterior, não é preciso armazenar os três inteiros de forma desnecessária.

range(10000000000) vs [i for i in range(10000000000)

Ou seja, caso você precise de apenas um elemento por vez nunca deixe de usar generators.

— Ok, e como funcionam?

Lvl 1: criando um generator.

O jeito mais simples de se criar um generator é em sua forma de função, utilizando a palavra reservada yield. Ela substitui a palavra reservada return, funcionando como um return que “armazena o contexto”. Cada vez que a função é chamada de novo, a execução é retomada a partir do último yield.

def sq(x):
for i in range(x):
yield i * i
for i in sq(10):
print(i) # 0*0, 1*1, 2*2, 3*3, ..., 9*9

O generator sq recebe como parâmetro um inteiro x e retorna (a cada chamada) o até o valor de x (excluso). O for se encarrega de diversas particularidades de como lidar com generators, mas eles já são utéis dessa forma.

Lvl 2: Lidando manualmente com um generator.

Da mesma maneira que é possível iterar uma sequêcia de elementos sem utilizar o for, e sim, combinando while, try e except (fica para um próximo post), é possível interagir com um generator.

s = sq(3)
# s
# <generator object sq at 0x7fb0c5bdc660>
next(s)
# 0
next(s)
# 1
next(s)
# 4
next(s)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# StopIteration

Utilizando a função next (built-in), você “pede” para um generator que ele execute todo o código a partir do último ponto, e até o próximo yield. Caso seja o último yield, o generator lança uma exceção StopIteration (o for lida com essa exceção para você).

É possível perceber que os valores não são armazenados e que, a cada chamada da função next(), o último valor é perdido. Btw, podemos ter diversos generators utilizando uma só função:

s1 = sq(3)
s2 = sq(3)
next(s1) # 0
next(s1) # 1
next(s2) # 0
next(s1) # 4
next(s2) # 1

Lvl 3: Generator expression.

Da mesma forma que é possível criar list comprehensions, é possível criar a versão generator da mesma operação, chamada de generator expression:

# list comp
lc = [i for i in range(10)]
# gen exp
lc = (i for i in range(10))

A sintaxe é praticamente a mesma, com exceção do uso de parêntesis no lugar de colchetes. Além disso, tudo o que é aplicável a list comprehensions, também funciona para generator expressions.

Lvl 4: Utilizar generators em conjunto com classes.

Fica para o próximo post.

Ou seja, sempre que não for necessário armazear todos os elementos de uma sequência, utilize generators.

Nos próximos posts, vou apresentar um problema na prática que é facilmente resolvido com o uso de generators.

tchau.

--

--