Julio Biason
2 years ago
5 changed files with 283 additions and 12 deletions
@ -0,0 +1,225 @@ |
|||||||
|
+++ |
||||||
|
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. |
@ -0,0 +1,11 @@ |
|||||||
|
+++ |
||||||
|
title = "Degenerated Brief Tin" |
||||||
|
date = 2023-01-04 |
||||||
|
|
||||||
|
[taxonomies] |
||||||
|
tags = ["projects", "personal", "mastodon", "activitypub", "download", "bookmarks"] |
||||||
|
+++ |
||||||
|
|
||||||
|
Download Bookmarked Toots. |
||||||
|
|
||||||
|
* [Megalodon](https://docs.rs/megalodon/latest/megalodon/index.html) |
Loading…
Reference in new issue