Pular para o conteúdo

    Capítulo 34, Intermediário

    Iteradores e geradores

    Um gerador produz valores sob demanda, um de cada vez. É a ferramenta que permite processar mais dados do que cabe na memória.

    Código deste capítulo: intermediario/cap34_geradores.py

    Iterável e iterador

    Um iterável é qualquer objeto que pode ser percorrido por um for (lista, string, arquivo). Um iterador é o objeto que entrega os itens um a um, com next(), e levanta StopIteration quando acaba. O for faz exatamente isso por baixo:

    intermediario/cap34_geradores.pylinhas 10 a 16
    lista = [1, 2, 3]
    iterador = iter(lista)
    print(next(iterador), next(iterador), next(iterador))
    try:
        next(iterador)
    except StopIteration:
        print("acabou")
    
    Saída
    1 2 3
    acabou
    

    Você pode criar o seu próprio iterador com uma classe que tenha __iter__ e __next__:

    intermediario/cap34_geradores.pylinhas 18 a 33
    class Contagem:
        def __init__(self, limite):
            self.limite = limite
            self.atual = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.atual >= self.limite:
                raise StopIteration
            self.atual += 1
            return self.atual
    
    
    print(list(Contagem(4)))
    
    Saída
    [1, 2, 3, 4]
    

    Funções geradoras com yield

    Escrever uma classe só para isso é trabalhoso. Uma função com yield vira um gerador automaticamente. Ela pausa a cada yield e retoma de onde parou:

    intermediario/cap34_geradores.pylinhas 38 a 47
    def contagem(limite):
        atual = 1
        while atual <= limite:
            yield atual
            atual += 1
    
    
    print(list(contagem(4)))
    gerador = contagem(2)
    print(next(gerador), next(gerador))
    
    Saída
    [1, 2, 3, 4]
    1 2
    

    Preguiça é economia

    Um gerador não guarda os itens. Ele guarda só o ponto em que parou. Por isso gerar cem mil números ocupa quase nada, enquanto a lista equivalente ocupa megabytes:

    intermediario/cap34_geradores.pylinhas 52 a 57
    import sys
    
    lista = [n for n in range(100_000)]
    gerador = (n for n in range(100_000))
    print(sys.getsizeof(gerador) < sys.getsizeof(lista))
    print(sum(gerador))
    
    Saída
    True
    4999950000
    

    Como o gerador não tem fim definido, ele pode até ser infinito, desde que quem consome pare no momento certo:

    intermediario/cap34_geradores.pylinhas 59 a 69
    from itertools import islice
    
    
    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    
    print(list(islice(fibonacci(), 10)))
    
    Saída
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    Pipelines

    Geradores se encadeiam: cada etapa recebe o gerador da anterior e processa um item por vez. Eu uso esse padrão para ler arquivos grandes: a linha entra, é filtrada, transformada, e só então a próxima é lida:

    intermediario/cap34_geradores.pylinhas 74 a 86
    def sem_vazias(linhas):
        for linha in linhas:
            if linha.strip():
                yield linha
    
    
    def numerar(linhas):
        for numero, linha in enumerate(linhas, start=1):
            yield f"{numero}: {linha}"
    
    
    texto = ["primeira", "", "segunda", "  ", "terceira"]
    print(list(numerar(sem_vazias(texto))))
    
    Saída
    ['1: primeira', '2: segunda', '3: terceira']
    

    Um gerador só pode ser percorrido uma vez

    Depois de esgotado, o gerador não volta. Esta é a pegadinha mais comum:

    intermediario/cap34_geradores.pylinhas 91 a 92
    g = (n for n in range(3))
    print(list(g), list(g))
    
    Saída
    [0, 1, 2] []
    

    Se você precisa percorrer duas vezes, guarde em uma lista ou recrie o gerador.

    Em lotes

    Desde o Python 3.12, itertools.batched divide qualquer iterável em grupos de tamanho fixo, o que é útil para enviar dados a uma API em blocos:

    intermediario/cap34_geradores.pylinhas 97 a 99
    from itertools import batched
    
    print(list(batched("abcdefg", 3)))
    
    Saída
    [('a', 'b', 'c'), ('d', 'e', 'f'), ('g',)]
    

    Exercício 1

    Dividir em lotes

    Escreva o gerador lotes(iteravel, tamanho) que devolva listas de até tamanho itens, usando islice.