Geradores em Python

Geradores em Python

Este artigo explica os geradores em Python.

YouTube Video

Geradores em Python

Visão geral

Os geradores em Python são um tipo de iterador e um recurso poderoso para realizar processamentos repetitivos de forma eficiente. Eles permitem escrever códigos eficientes em memória ao lidar com grandes volumes de dados.

O que é um gerador?

Um gerador em Python é uma função especial que produz um valor de cada vez, definida usando a palavra-chave yield. Sua característica é pausar a execução enquanto mantém seu estado e pode ser retomada posteriormente.

Noções básicas do yield

yield é uma palavra-chave que retorna um valor e pausa a execução da função ao mesmo tempo.

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8print(next(gen))  # 1
 9print(next(gen))  # 2
10print(next(gen))  # 3
  • Quando chamada, esta função retorna um objeto gerador que fornece valores um por um.
  • Se você chamar next() quando não houver próximo valor, ocorrerá um erro StopIteration.

next() e StopIteration

 1def simple_generator():
 2    yield 1
 3    yield 2
 4    yield 3
 5
 6gen = simple_generator()
 7
 8try:
 9    while True:
10        value = next(gen)
11        print(value)
12except StopIteration:
13    print("Finished")
  • Ao tratar explicitamente o erro StopIteration desta forma, você pode detectar quando um gerador terminou.

send(value)

Chamar send(value) retoma o gerador e envia value para a posição da expressão yield. O valor enviado pode ser recebido do lado do gerador como valor de retorno da expressão yield. Na primeira chamada, não é possível enviar nada além de None com send(value), portanto, você deve usar next() ou send(None).

 1def gen():
 2    x = yield 1
 3    print(f"x = {x}")
 4    y = yield 2
 5    print(f"y = {y}")
 6
 7g = gen()
 8print(next(g))       # -> 1 (value from yield 1)
 9print(g.send(10))    # -> x = 10, 2 (value from yield 2)
10print(g.send(20))    # -> y = 20, StopIteration occurs
  • Com send(10), o yield do gerador se torna uma expressão que retorna 10, e 10 é atribuído a x.

throw()

Chamar throw retoma o gerador e lança uma exceção na posição do yield pausado. Você pode tratar a exceção dentro do gerador para continuar o processamento. Se a exceção não for capturada, ela se propaga para fora e o gerador é encerrado.

 1def gen():
 2    try:
 3        yield 1
 4    except ValueError as e:
 5        print(f"Caught: {e}")
 6        yield "recovered"
 7
 8g = gen()
 9print(next(g))   # -> 1
10print(g.throw(ValueError("boom")))  # -> Caught: boom, "recovered"
  • Neste código, throw é chamado para injetar uma exceção no gerador. Do lado do gerador, a exceção é tratada e recovered é retornado.

close()

Chamar close() encerra o gerador. Dentro do gerador, você pode executar a limpeza usando finally. Chamar next() ou send() após chamar close() levanta um erro StopIteration.

1def gen():
2    try:
3        yield 1
4    finally:
5        print("Cleaning up...")
6
7g = gen()
8print(next(g))  # -> 1
9g.close()       # -> Cleaning up...
  • Este código mostra que chamar close() encerra o gerador e dispara o processo de limpeza em finally.

yield from

yield from é uma sintaxe usada para delegar a um subgerador. É uma maneira simples de chamar outro gerador dentro de um gerador e passar todos os seus valores para o escopo externo.

1def sub_gen():
2    yield 1
3    yield 2
4
5def main_gen():
6    yield from sub_gen()
7    yield 3
8
9print(list(main_gen()))  # -> [1, 2, 3]
  • Este código delega todos os valores do subgerador ao gerador externo usando yield from e então produz 3.

Relação com iteradores

Geradores implementam internamente __iter__() e __next__(), tornando-os um tipo de iterador. Portanto, eles são totalmente compatíveis com operações iteráveis como loops for.

Integração com loops for

Em Python, um loop for usa internamente o next() para recuperar valores automaticamente.

1def simple_generator():
2    yield 1
3    yield 2
4    yield 3
5
6for value in simple_generator():
7    print(value)

Dessa forma, o tratamento de StopIteration também é automático.

Criando geradores infinitos

1def count_up(start=0):
2    while True:
3        yield start
4        start += 1
5
6counter = count_up()
7print(next(counter))  # 0
8print(next(counter))  # 1

É possível criar laços infinitos, mas deve-se ter cuidado ao utilizá-los.

Expressões de gerador

Expressões de gerador, escritas com parênteses, permitem definir geradores com sintaxe semelhante às compreensões de listas.

1# List comprehension (generates the entire list at once)
2squares_list = [x**2 for x in range(5)]
3print(squares_list)
4
5# Generator expression
6squares_gen = (x**2 for x in range(5))
7for square in squares_gen:
8    print(square)

Ao contrário das compreensões de listas, elas não carregam todos os elementos na memória de uma só vez, tornando-se mais eficientes em termos de memória.

Tratamento de erros em geradores

Exceções podem ocorrer dentro de um gerador. Nesses casos, você usa o try-except como em um código Python normal.

 1def safe_divide_generator(numbers, divisor):
 2    """Yields results of dividing numbers by a given divisor safely."""
 3    for number in numbers:
 4        try:
 5            yield number / divisor  # Attempt to divide and yield result.
 6        except ZeroDivisionError:
 7            yield float('inf')  # Return infinity if division by zero occurs.
 8
 9# Example usage
10numbers = [10, 20, 30]
11gen = safe_divide_generator(numbers, 0)  # Create generator with divisor as 0.
12for value in gen:
13    print(value)  # Output: inf, inf, inf

Neste exemplo, o tratamento adequado de erros é realizado no caso de uma divisão por zero.

Rastreamento de pilha de um gerador

Se uma exceção ocorrer dentro do gerador, ela será lançada quando o gerador for retomado.

 1def error_generator():
 2    """A generator that yields values and raises an error."""
 3    yield 1
 4    raise ValueError("An error occurred")  # Raise a ValueError intentionally.
 5    yield 2
 6
 7gen = error_generator()
 8print(next(gen))       # Output: 1 (first value yielded)
 9try:
10    print(next(gen))   # Attempt to get the next value, which raises an error
11except ValueError as e:
12    print(e)           # Output: An error occurred (exception message is printed)
  • Este gerador retorna 1 primeiro. O erro lançado ao retomar é capturado e exibido como uma mensagem de erro.

Exemplos de uso de geradores

Lendo um arquivo linha por linha (adequado para arquivos grandes)

1def read_large_file(filepath):
2    with open(filepath, 'r') as f:
3        for line in f:
4            yield line.strip()
  • Esta função lê um arquivo de texto linha por linha usando um iterador, remove os espaços em branco de cada linha e retorna como um gerador, permitindo que arquivos grandes sejam processados com baixo uso de memória.

Gerador para a sequência de Fibonacci

1def fibonacci(limit):
2    a, b = 0, 1
3    while a < limit:
4        yield a
5        a, b = b, a + b
6
7for n in fibonacci(100):
8    print(n)
  • Este código usa um gerador para gerar sequencialmente números de Fibonacci menores que o limite superior e os imprime usando um laço for.

Casos de uso

Os geradores também podem ser usados nos seguintes cenários.

  • Processamento sequencial de arquivos CSV ou de log grandes
  • Paginação de API
  • Processamento de dados em streaming (por exemplo, Kafka, dispositivos IoT)

Resumo

Conceito Ponto chave
yield Pausa e retorna um valor
Função geradora Uma função que contém yield e retorna um iterador quando chamada
Vantagens Eficiente em memória e ideal para processar grandes conjuntos de dados
Expressão de gerador Permite uma sintaxe concisa como (x for x in iterable)

Ao usar geradores, você pode processar grandes conjuntos de dados de forma eficiente, economizando memória e mantendo seu código conciso.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video