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 blocowith. Deve retornar o recurso que será vinculado à variávelas.__exit__(self, exc_type, exc_val, exc_tb): executado ao sair do bloco, independentemente de exceções. Recebe informações sobre exceções (ouNonese 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.ExitStackpara 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/finallysempre 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
- PEP 343 – The “with” Statement — Documento oficial que introduziu a declaração
withe o protocolo de context managers em Python. - Python Documentation: The with statement — Documentação oficial sobre a sintaxe e semântica do
with. - Python Documentation: contextlib — Utilities for with-statement contexts — Documentação completa do módulo
contextlibcom todos os utilitários. - Real Python: Context Managers and Python's with Statement — Tutorial abrangente com exemplos práticos e casos de uso avançados.
- GeeksforGeeks: Context Managers in Python — Guia explicativo com implementações de classe e generator-based.
- Effective Python: Item 66: Consider contextlib and with Statements — Dicas de boas práticas e padrões de uso do
withecontextlib.