The source content for blog.juliobiason.me
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

226 lines
6.8 KiB

+++
title = "Pyngos de Python I"
date = 2023-03-27
[taxonomies]
tags = ["python", "generators"]
+++
"Pyngos de Python" são pequenas explicações de Python.
Nesse post, vamos falor sobre generators.
<!-- more -->
Vamos começar falando sobre list comprehensions, que são bem comuns em Python.
De forma gera, um list comprehension é definido como
```python
[transformação
for variável
in iterável
if condição]
```
* `iterável` é o container com os elementos que queremos percorrer;
* `variável` define qual vai ser o nome da variável que vamos lidar cada um dos
elementos do `iterável`;
* `transformação` é qualquer transformação que queremos fazer sobre `variável`;
* `condição` é um opcional caso queiremos processar apenas alguns elementos.
Um exemplo de list comprehension em ação:
```python
lista = [1, 2, 3, 4]
lc = [i * 2 for i in lista]
print(lc) # [2, 4, 6, 8]
```
Embora útil, existe um problema: List comprehensions geram uma lista com, no
máximo, o mesmo tamanho do iterável original; se você tiver um array de 500.000
elementos, um list comprehension que não tenha uma condição vai gerar outro
array com 500.000 elementos.
E, em alguns casos, isso não é necessário.
Antes de ver onde generators podem ser usados, veremos a sintaxe de um:
```python
(transformação
for variável
in iterável
if condição)
```
Como pode ser visto, a sintaxe é bem semelhante; a diferença é que
comprehensions usam `[]`, enquanto generators usam `()`.
E como exemplo:
```python
lista = [1, 2, 3, 4]
gen = (i * 2 for i in lista)
print(gen) # <generator object <genexpr> at 0x7f7f30843df0>
```
O que diabos é esse `generator object`?
Generators não geram os dados todos numa passada; os dados somente são
processados quando pedidos. A forma de pedir o próximo elemento é usando a
função `next`; quando o generator encontra o final do iterável, ele levanta a
exceção `StopIteration`:
```python
lista = [1, 2, 3, 4]
gen = (i * 2 for i in lista)
print(next(gen)) # 2
print(next(gen)) # 4
print(next(gen)) # 6
print(next(gen)) # 8
print(next(gen)) # Exceção: StopIteration
```
Curiosamente, `for` sabe lidar com `StopIteration` e `next()`, o que torna
possível usar um generator diretamente no `for`:
```python
lista = [1, 2, 3, 4]
for i in (i * 2 for i in l):
print(i) # 2, 4, 6, 8
# Nenhuma exceção aqui.
```
Mas é a vantagem de usar generators?
A primeira vantagem pode ser vista no `for` acima: Imagine que `lista` tem
500.000 elementos. Usar list comprehensions não mudaria nada no código (com a
exceção de usar `[]` ao invés de `()`), mas estamos gerando a multiplicação
somente quando necessário. Agora imagine que estamos procurando algo na lista
original e vamos parar assim que encontrarmos o registro: com list
comprehension, a nova lista será sempre gerada, e se o o elemento procurado for
o primeiro, acabamos gerando 499.999 elementos que não vamos usar. Com
generators, no momento que encerramos a procura, nada mais é gerado -- e
somente o elemento procurado é gerado.
Um exemplo mais real: Arquivos são iteráveis, onde cada requisição é uma linha
do arquivo. Se o arquivo sendo processado é um CSV, podemos fazer um generator
que separa os campos sobre a iteração do arquivo enquanto procuramos um
registro específico:
```python
with open('arquivo.csv') as origem:
for registro in (linha.split(',') for linha in origem):
if registro[1] == 'entrada':
return registro[2]
```
Neste código, estamos procurando a linha do CSV cujo 2o elemento (listas
começam em 0) tem o valor "entrada"; quando encontrarmos, retornamos o valor da
coluna seguinte. A medida que o `for` for pedindo valores, o generator é
chamado; o generator que criamos quebra a linha usando "," como separador; como
o generator usa o iterável do arquivo (que, por baixo dos panos, também é um
generator), somente quando for pedido um registro é que uma linha será lida;
somente quando a linha vier é que vai ser feito o split. E se, por algum
motivo, o registro procurando for o primeiro, foi somente lida uma linha do
arquivo[^1] e feito o split somente uma vez.
## BÔNUS: Generator Functions!
Existe uma forma de criar uma função que age como um generator, usando o
statement `yield`, da mesma forma que se usaria o statement `return`. A
diferença é que quando o Python encontra `yield`, ao invés de destruir tudo que
estava na função, ele guarda a posição atual e, na chamada do `next()`,
continua naquela posição.
Por exemplo, se tivermos:
```python
def double(lista):
for i in lista:
return i * 2
double([1, 2, 3, 4])
```
Irá retornar apenas `2` porque, ao ver o `return`, o Python vai destruir tudo
que a função já fez e retornar o valor indicado -- incluindo encerrar o `for`
antes de chegar no final.
Com generator functions, teríamos:
```python
def double(lista):
for i in lista:
return i
gen = double([1, 2, 3, 4])
next(gen) # 2
next(gen) # 4
next(gen) # 6
next(gen) # 8
next(gen) # StopIteration
```
Note que a chamada para a função é que retorna um generator. Tentar fazer
```python
def double(lista):
for i in lista:
return i
next(double([1, 2, 3, 4])) # 2
next(double([1, 2, 3, 4])) # 2
next(double([1, 2, 3, 4])) # 2
next(double([1, 2, 3, 4])) # 2
...
```
... vai gerar um novo generator a cada chamada.
Ainda, é possível que a função tenha mais de um `yield`:
```python
def double(lista):
yield lista[0] * 2
yield lista[1] * 2
yield lista[2] * 2
gen = double([4, 3, 2, 1])
next(gen) # 8
next(gen) # 6
next(gen) # 4
next(gen) # StopIteration
```
Aqui, a primeira chamada de `next()` vai retornar o valor do primeiro `yield`,
que é o primeiro elemento da lista multiplicado por 2; o próximo `next()` vai
executar o comando logo depois do primeiro `yield`, que é o segundo `yield`; e
a terceira chamada vai continuar a execução logo depois desse, que é o terceiro
`yield`. Como o código termina aí, o generator vai levantar a exceção
`StopIteration`.
Mas o que aconteceria se... a função nunca retornasse nada?
```python
def gen():
i = 0
while True:
yield i * 2
i += 1
```
Neste caso, usando `next()` no generator, a primeira vez será retornado "0"; o
`next()` seguinte irá continuar o código, somando "1" ao nosso contador,
retornando para o começo do loop e retornando "2"; e assim sucessivamente até o
fim do mundo (ou até ser pressionado Ctrl+C, desligado o computador ou atingido
o número máximo permitido para inteiros em Python).
---
[^1]: Tecnicamente, vai ser lido mais, porque o Python usa "buffers" de
leitura, carregando blocos e depois enviando apenas os bytes desde a última
posição lida até o caracter de nova linha. Mas, para simplificar as coisas,
imaginem que apenas uma linha é lida mesmo.