Context managers: with e enter/exit

1. Introdução aos Context Managers

Context managers são uma das features mais elegantes do Python para gerenciamento automático de recursos. Eles resolvem um problema clássico: a gestão manual de recursos como arquivos abertos, conexões de banco de dados, locks de threading e transações.

Sem context managers, o desenvolvedor precisa lembrar de fechar explicitamente cada recurso, o que leva a código verboso e propenso a vazamentos de memória ou deadlocks:

# Abordagem manual - propensa a erros
arquivo = open('dados.txt', 'r')
try:
    dados = arquivo.read()
finally:
    arquivo.close()

O bloco with atua como açúcar sintático que automatiza a aquisição e liberação de recursos, garantindo que mesmo em caso de exceções, a limpeza seja executada.

2. A Declaração with na Prática

A sintaxe básica do with é intuitiva:

with expressão as variável:
    # bloco de código

O exemplo clássico é o gerenciamento de arquivos:

with open('dados.txt', 'r') as arquivo:
    conteudo = arquivo.read()
    print(conteudo)
# Arquivo é automaticamente fechado aqui

Python também permite múltiplos context managers em um único with:

# Aninhamento tradicional
with open('entrada.txt', 'r') as entrada:
    with open('saida.txt', 'w') as saida:
        saida.write(entrada.read())

# Sintaxe compacta (Python 2.7+)
with open('entrada.txt', 'r') as entrada, open('saida.txt', 'w') as saida:
    saida.write(entrada.read())

3. O Protocolo de Context Manager: __enter__ e __exit__

O protocolo de context manager consiste em dois métodos mágicos:

  • __enter__(self): executado ao entrar no bloco with. Deve retornar o recurso que será vinculado à variável as.
  • __exit__(self, exc_type, exc_val, exc_tb): executado ao sair do bloco, independentemente de exceções. Recebe informações sobre exceções (ou None se não houve erro).
class MeuContexto:
    def __enter__(self):
        print("Entrando no contexto")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Saindo do contexto")
        if exc_type:
            print(f"Exceção capturada: {exc_type.__name__}: {exc_val}")
            # Retornar True suprime a exceção, False ou None a propaga
            return False
        return True

with MeuContexto() as ctx:
    print("Dentro do contexto")

O método __exit__ pode suprimir exceções retornando True. Caso contrário, a exceção é propagada normalmente após a saída do bloco.

4. Implementando um Context Manager Personalizado (Classe)

Vamos criar um gerenciador de temporizador para medir tempo de execução:

import time

class Temporizador:
    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:.4f} segundos")
        return False

# Uso
with Temporizador():
    total = sum(range(10**7))

Outro exemplo prático: gerenciador de transação de banco de dados simulada:

class TransacaoBanco:
    def __init__(self, conexao):
        self.conexao = conexao

    def __enter__(self):
        print("Iniciando transação")
        self.conexao.iniciar_transacao()
        return self.conexao

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(f"Erro detectado: {exc_val}. Realizando rollback.")
            self.conexao.rollback()
        else:
            print("Transação bem-sucedida. Realizando commit.")
            self.conexao.commit()
        return False  # Propaga exceções

# Simulação de uso
class ConexaoSimulada:
    def iniciar_transacao(self): print("Transação iniciada")
    def commit(self): print("Commit realizado")
    def rollback(self): print("Rollback realizado")

with TransacaoBanco(ConexaoSimulada()) as conn:
    print("Executando operações no banco...")
    # Se ocorrer exceção aqui, rollback é automático

5. O Módulo contextlib: Ferramentas e Abstrações

O módulo contextlib oferece utilitários poderosos:

from contextlib import contextmanager, closing, suppress, redirect_stdout
import io

# contextmanager - decorador para criar context managers com generators
@contextmanager
def arquivo_temporario(nome, modo):
    print(f"Abrindo {nome}")
    arquivo = open(nome, modo)
    try:
        yield arquivo
    finally:
        print(f"Fechando {nome}")
        arquivo.close()

# closing - fecha objetos com método close()
with closing(open('dados.txt', 'w')) as f:
    f.write('conteúdo')

# suppress - suprime exceções específicas
with suppress(FileNotFoundError):
    open('arquivo_inexistente.txt', 'r')

# redirect_stdout - redireciona saída padrão
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("Esta mensagem vai para o buffer")

6. Context Managers com @contextmanager (Generator-Based)

A abordagem baseada em generator é mais concisa para context managers simples:

from contextlib import contextmanager

@contextmanager
def gerenciador_temporizador():
    inicio = time.perf_counter()
    try:
        yield  # yield None ou o recurso
    finally:
        fim = time.perf_counter()
        print(f"Tempo: {fim - inicio:.4f}s")

# Uso
with gerenciador_temporizador():
    time.sleep(0.5)

Comparação: classes são melhores para context managers complexos com estado interno; generators são ideais para casos simples e diretos.

7. Erros Comuns e Boas Práticas

Erro 1: Esquecer de tratar exceções no __exit__

class GerenciadorRuim:
    def __exit__(self, *args):
        pass  # Exceções são propagadas por padrão

Erro 2: Reutilizar contexto após o bloco with

with open('dados.txt') as f:
    conteudo = f.read()
# f está fechado aqui!
# f.read() causaria ValueError

Boas práticas:

  • Use contextlib.ExitStack para gerenciamento dinâmico de múltiplos contextos:
from contextlib import ExitStack

with ExitStack() as stack:
    arquivos = [stack.enter_context(open(f'nome_{i}.txt', 'w')) for i in range(5)]
    # Todos os arquivos são fechados automaticamente
  • Prefira context managers a try/finally sempre que possível para maior legibilidade.

8. Casos de Uso Avançados

Locks em threading:

import threading
lock = threading.Lock()

with lock:
    # Seção crítica - lock é adquirido e liberado automaticamente
    recurso_compartilhado += 1

Mudança temporária de diretório:

import os
from contextlib import contextmanager

@contextmanager
def mudar_diretorio(destino):
    origem = os.getcwd()
    os.chdir(destino)
    try:
        yield
    finally:
        os.chdir(origem)

with mudar_diretorio('/tmp'):
    print(os.getcwd())  # /tmp
print(os.getcwd())  # Diretório original

Mocking em testes unitários:

from unittest.mock import patch

with patch('minha_api.externa.chamar') as mock_chamar:
    mock_chamar.return_value = {'status': 'ok'}
    resultado = minha_funcao()
    mock_chamar.assert_called_once()

Context managers são fundamentais para implementar padrões de design como transações, sessões e conexões de forma segura e elegante em Python.

Referências