Context managers (gerenciadores de contexto) são uma das ferramentas mais elegantes e poderosas do Python. Eles permitem gerenciar recursos — como arquivos, conexões de rede, locks e transações — de forma limpa e segura, garantindo que a limpeza seja feita mesmo em caso de erro. Nesta aula, vamos entender profundamente como eles funcionam, como usá-los e como criar os seus próprios.

Se você já usou a instrução with para abrir arquivos, já se beneficiou de um context manager. Mas há muito mais: podemos controlar a entrada e saída de um bloco de código, tratar exceções, criar timers, medir desempenho e até gerenciar transações de banco de dados. Vamos mergulhar nos detalhes.

O protocolo (__enter__/__exit__)

Um context manager é um objeto que implementa o protocolo de gerenciamento de contexto, composto por dois métodos especiais: __enter__ e __exit__. O método __enter__ é chamado quando entramos no bloco with e seu valor de retorno é atribuído à variável após as. O método __exit__ é chamado ao sair do bloco, seja normalmente ou devido a uma exceção.

O método __exit__ recebe três argumentos: o tipo da exceção (ou None se não houve erro), o valor da exceção e o traceback. Ele deve retornar um booleano: se retornar True, a exceção é suprimida (não é propagada); se retornar False ou None, a exceção é propagada. Entender esses detalhes é essencial para criar gerenciadores robustos.

class MeuContexto:
    def __enter__(self):
        print("Entrando no contexto")
        return 42  # valor atribuído à variável do with

    def __exit__(self, exc_type, exc_value, traceback):
        print("Saindo do contexto")
        if exc_type is not None:
            print(f"Exceção capturada: {exc_type.__name__}: {exc_value}")
        # Retorna False para propagar exceções
        return False

with MeuContexto() as valor:
    print(f"Dentro do contexto, valor = {valor}")
# Saída:
# Entrando no contexto
# Dentro do contexto, valor = 42
# Saindo do contexto

No exemplo acima, o __enter__ retorna o número 42, que é atribuído a valor. O __exit__ é chamado ao sair, e como não há exceção, os argumentos são None. Se houvesse uma exceção, poderíamos decidir se ela seria suprimida ou não.

with

A instrução with é a forma mais comum de usar context managers. Ela garante que o método __exit__ seja chamado automaticamente, mesmo se ocorrer uma exceção dentro do bloco. Isso é conhecido como "gerenciamento de recursos" e elimina a necessidade de blocos try/finally manuais.

O uso mais clássico é com arquivos: abrir um arquivo com with open() garante que ele será fechado corretamente, mesmo se ocorrer um erro durante a leitura ou escrita. Mas podemos usar with com qualquer objeto que implemente o protocolo. Além disso, o with pode ser usado com múltiplos context managers em uma única linha, facilitando o gerenciamento de vários recursos simultaneamente.

# Exemplo clássico: arquivo
with open("dados.txt", "w") as arquivo:
    arquivo.write("Olá, mundo!")
# O arquivo é fechado automaticamente após o bloco

# Múltiplos context managers
with open("origem.txt", "r") as entrada, open("destino.txt", "w") as saida:
    saida.write(entrada.read())

# Simulando um contexto personalizado
class Recurso:
    def __enter__(self):
        print("Abrindo recurso")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Fechando recurso")

with Recurso() as r:
    print("Usando recurso")
# Saída:
# Abrindo recurso
# Usando recurso
# Fechando recurso

Observe que, ao usar with, não precisamos nos preocupar em fechar o recurso manualmente. Isso torna o código mais limpo e menos propenso a vazamentos de recursos.

contextlib

A biblioteca contextlib fornece utilitários para trabalhar com context managers de forma mais conveniente. Ela inclui classes e decoradores que simplificam a criação de gerenciadores de contexto, especialmente para casos simples. Os mais importantes são contextlib.contextmanager, contextlib.closing, contextlib.suppress e contextlib.ExitStack.

O decorador @contextmanager permite criar um context manager a partir de uma função geradora. Dentro da função, usamos yield para marcar o ponto de entrada e saída: o código antes do yield é executado no __enter__, e o código após o yield é executado no __exit__. Isso elimina a necessidade de criar uma classe completa para context managers simples.

from contextlib import contextmanager

@contextmanager
def meu_gerenciador():
    print("Entrando")
    try:
        yield "valor"
    finally:
        print("Saindo")

with meu_gerenciador() as valor:
    print(f"Dentro: {valor}")
# Saída:
# Entrando
# Dentro: valor
# Saindo

Outros utilitários: closing chama o método close() de um objeto ao sair do bloco, útil para objetos que possuem close() mas não implementam o protocolo. suppress permite suprimir exceções específicas dentro do bloco, de forma concisa. ExitStack permite gerenciar dinamicamente múltiplos context managers, empilhando-os e garantindo que todos sejam fechados na ordem inversa.

from contextlib import closing, suppress, ExitStack

# closing
class Conexao:
    def close(self):
        print("Conexão fechada")

with closing(Conexao()) as c:
    print("Usando conexão")

# suppress
with suppress(FileNotFoundError):
    open("arquivo_inexistente.txt")
    print("Arquivo não existe, mas sem erro!")

# ExitStack
with ExitStack() as stack:
    arquivos = [stack.enter_context(open(f"arquivo_{i}.txt", "w")) for i in range(3)]
    for f in arquivos:
        f.write("dados")
    print("Arquivos abertos")
# Todos os arquivos são fechados ao sair

Criando os seus

Criar seus próprios context managers é uma ótima forma de encapsular lógica de configuração e limpeza, tornando o código mais legível e reutilizável. Há duas abordagens principais: implementar uma classe com __enter__ e __exit__, ou usar o decorador @contextmanager. A escolha depende da complexidade e da necessidade de reuso.

Vamos criar um gerenciador para medir o tempo de execução de um bloco de código. Com uma classe, podemos armazenar o tempo inicial e calcular a duração no __exit__. Com @contextmanager, podemos usar yield e um finally para calcular o tempo. Ambos são válidos; o importante é entender o protocolo.

import time

# Abordagem com classe
class Timer:
    def __enter__(self):
        self.inicio = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.fim = time.perf_counter()
        self.duracao = self.fim - self.inicio
        print(f"Tempo decorrido: {self.duracao:.6f} segundos")
        return False

with Timer() as t:
    time.sleep(0.1)
# Saída: Tempo decorrido: ~0.100xxx segundos

# Abordagem com @contextmanager
@contextmanager
def timer():
    inicio = time.perf_counter()
    try:
        yield
    finally:
        fim = time.perf_counter()
        print(f"Tempo decorrido: {fim - inicio:.6f} segundos")

with timer():
    time.sleep(0.1)
# Saída similar

Outro exemplo útil: um gerenciador que muda o diretório de trabalho temporariamente. Ele deve salvar o diretório atual, mudar para o novo e, ao sair, restaurar o original. Isso é útil em scripts que precisam operar em vários diretórios.

import os

@contextmanager
def mudar_diretorio(caminho):
    diretorio_original = os.getcwd()
    os.chdir(caminho)
    try:
        yield
    finally:
        os.chdir(diretorio_original)

with mudar_diretorio("/tmp"):
    print(os.getcwd())  # /tmp
print(os.getcwd())  # volta ao original

Ao criar seus próprios context managers, lembre-se de tratar exceções adequadamente. Se o __exit__ retornar True, a exceção é suprimida; se retornar False, ela é propagada. Essa decisão deve ser consciente, pois suprimir exceções pode esconder erros importantes.

Boas práticas e observações finais

Use context managers sempre que precisar gerenciar recursos que devem ser liberados (arquivos, conexões, locks). Prefira with a blocos try/finally manuais, pois é mais legível e menos propenso a erros. Ao criar seus próprios, documente claramente o que acontece na entrada e saída, e decida conscientemente sobre a supressão de exceções.

Além disso, explore a biblioteca contextlib para simplificar casos comuns. Ela oferece ferramentas poderosas como ExitStack para gerenciamento dinâmico e nullcontext para quando você precisa de um context manager que não faz nada (útil para códigos condicionais).

Exercícios

  1. Implemente um context manager usando uma classe que imprima "Início" ao entrar e "Fim" ao sair, mas que também capture e exiba qualquer exceção que ocorra dentro do bloco.

    ✓ Resposta:
    class MeuContexto:
        def __enter__(self):
            print("Início")
            return self
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("Fim")
            if exc_type is not None:
                print(f"Exceção capturada: {exc_type.__name__}: {exc_val}")
            return False  # propaga exceção
    
    with MeuContexto():
        raise ValueError("erro")
    # Saída:
    # Início
    # Fim
    # Exceção capturada: ValueError: erro
  2. Use contextlib.contextmanager para criar um gerenciador que receba uma lista de arquivos e os abra todos em modo escrita, garantindo que sejam fechados ao sair. Apresente o uso com três arquivos.

    ✓ Resposta:
    from contextlib import contextmanager
    
    @contextmanager
    def abrir_arquivos(nomes):
        arquivos = [open(nome, "w") for nome in nomes]
        try:
            yield arquivos
        finally:
            for f in arquivos:
                f.close()
    
    with abrir_arquivos(["a.txt", "b.txt", "c.txt"]) as arquivos:
        for f in arquivos:
            f.write("teste")
    # Arquivos são fechados automaticamente
  3. Crie um context manager que altere o diretório de trabalho para um dado caminho e, ao sair, restaure o original. Use a biblioteca os e trate possíveis exceções.

    ✓ Resposta:
    import os
    from contextlib import contextmanager
    
    @contextmanager
    def mudar_diretorio(caminho):
        original = os.getcwd()
        os.chdir(caminho)
        try:
            yield
        finally:
            os.chdir(original)
    
    with mudar_diretorio("/tmp"):
        print(os.getcwd())
    print(os.getcwd())
  4. Explique a diferença entre retornar True e False no método __exit__. Dê um exemplo onde retornar True é útil.

    ✓ Resposta: Retornar True suprime a exceção, ou seja, ela não é propagada para o chamador. Retornar False permite que a exceção continue a se propagar. Um exemplo útil é um context manager que ignora exceções específicas, como FileNotFoundError, permitindo que o programa continue sem interrupção.
    class IgnoraErro:
        def __enter__(self):
            return self
        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is FileNotFoundError:
                print("Arquivo não encontrado, mas continuando...")
                return True
            return False
    
    with IgnoraErro():
        open("nao_existe.txt")
    print("Fim")  # Este print é executado porque a exceção foi suprimida
  5. Usando ExitStack, escreva um código que abra dois arquivos (um para leitura e outro para escrita) e copie o conteúdo do primeiro para o segundo, garantindo que ambos sejam fechados corretamente.

    ✓ Resposta:
    from contextlib import ExitStack
    
    with ExitStack() as stack:
        origem = stack.enter_context(open("origem.txt", "r"))
        destino = stack.enter_context(open("destino.txt", "w"))
        destino.write(origem.read())
    # Ambos os arquivos são fechados automaticamente ao sair do with

Referências