Context Manager em Python: O que É e Como Funciona | Python Brasil
Aprenda context managers em Python: with, __enter__, __exit__, contextlib, async, ExitStack, suppress e padrões reais. Guia completo com exemplos.
O que é um Context Manager?
Um context manager é um objeto que define o comportamento de entrada e saída de um bloco with. Ele garante que recursos (como arquivos, conexões de banco de dados ou locks) sejam corretamente liberados após o uso, mesmo se ocorrer uma exceção.
É um dos padrões mais elegantes do Python para gerenciamento seguro de recursos, eliminando erros de “esqueceu de fechar” que ocorrem com código manual de cleanup.
O bloco with: por que ele existe
# Sem context manager — propenso a erros
f = open('dados.txt', 'r')
try:
conteudo = f.read()
processar(conteudo)
finally:
f.close() # Precisamos do try/finally para garantir o fechamento
# Com context manager — limpo e seguro
with open('dados.txt', 'r') as f:
conteudo = f.read()
processar(conteudo)
# Arquivo fechado automaticamente, mesmo com exceção!
O bloco with é a forma idiomática Python de garantir cleanup automático de recursos.
Criando context managers com classe
Qualquer classe que implemente __enter__ e __exit__ pode ser usada como context manager:
import time
class MedidorTempo:
def __init__(self, nome):
self.nome = nome
self.inicio = None
self.duracao = None
def __enter__(self):
self.inicio = time.perf_counter()
return self # O valor retornado fica disponível no 'as'
def __exit__(self, exc_type, exc_val, exc_tb):
self.duracao = time.perf_counter() - self.inicio
print(f"{self.nome}: {self.duracao:.4f}s")
# exc_type, exc_val, exc_tb são None se não houve exceção
# Retornar True suprime a exceção; False (ou None) a propaga
return False
with MedidorTempo("Processamento") as m:
time.sleep(0.5)
dados = [x**2 for x in range(100_000)]
print(f"Duração armazenada: {m.duracao:.4f}s")
# Processamento: 0.5123s
O método __exit__ recebe três argumentos sobre possíveis exceções: o tipo, o valor e o traceback. Se retornar True, a exceção é suprimida; qualquer outro valor faz a exceção propagar normalmente.
Criando com @contextmanager (mais simples)
O decorator contextlib.contextmanager transforma uma função geradora em context manager, eliminando a necessidade de criar uma classe:
from contextlib import contextmanager
import tempfile
import os
import shutil
@contextmanager
def diretorio_temporario():
"""Context manager para criar e limpar diretório temporário."""
tmpdir = tempfile.mkdtemp()
try:
yield tmpdir # O código do bloco 'with' executa aqui
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
@contextmanager
def transacao_banco(conexao):
"""Context manager para transações de banco com commit/rollback."""
try:
yield conexao
conexao.commit()
print("Transação confirmada")
except Exception as e:
conexao.rollback()
print(f"Transação desfeita: {e}")
raise
with diretorio_temporario() as tmpdir:
# Trabalha com arquivos no diretório temporário
arquivo = os.path.join(tmpdir, "dados.txt")
with open(arquivo, 'w') as f:
f.write("dados temporários")
print(f"Arquivo em: {arquivo}")
# Diretório limpo automaticamente ao sair do bloco
Context managers assíncronos
Para código assíncrono com async/await, use context managers assíncronos com __aenter__ e __aexit__, ou @asynccontextmanager:
import asyncio
from contextlib import asynccontextmanager
class ConexaoAssincrona:
def __init__(self, host):
self.host = host
self.conexao = None
async def __aenter__(self):
print(f"Conectando assincronamente a {self.host}...")
await asyncio.sleep(0.1) # Simula latência de conexão
self.conexao = f"conn_{self.host}"
return self.conexao
async def __aexit__(self, exc_type, exc_val, exc_tb):
print("Fechando conexão assíncrona...")
await asyncio.sleep(0.05) # Simula cleanup
self.conexao = None
return False
@asynccontextmanager
async def sessao_http(url):
"""Context manager assíncrono para sessões HTTP."""
print(f"Abrindo sessão para {url}")
sessao = {"url": url, "ativa": True}
try:
yield sessao
finally:
sessao["ativa"] = False
print(f"Sessão para {url} fechada")
async def main():
async with ConexaoAssincrona("postgres://localhost") as conn:
print(f"Usando: {conn}")
async with sessao_http("https://api.exemplo.com") as sessao:
print(f"Sessão ativa: {sessao['ativa']}")
asyncio.run(main())
contextlib: utilitários poderosos
O módulo contextlib oferece vários utilities prontos:
from contextlib import (
suppress,
redirect_stdout,
redirect_stderr,
ExitStack,
closing,
nullcontext,
)
import io
# suppress: ignora exceções específicas
with suppress(FileNotFoundError):
os.remove("arquivo_que_pode_nao_existir.tmp")
# Nenhum erro — FileNotFoundError foi suprimido
# redirect_stdout: captura saída padrão
saida = io.StringIO()
with redirect_stdout(saida):
print("Isso vai para a StringIO, não para o terminal")
print("Linha 2")
conteudo = saida.getvalue()
print(f"Capturado: {conteudo!r}")
# closing: para objetos com .close() mas sem suporte a 'with'
class RecursoLegado:
def buscar(self): return "dados"
def close(self): print("Recurso legado fechado")
with closing(RecursoLegado()) as r:
dados = r.buscar()
# Chamou r.close() automaticamente
# nullcontext: context manager que não faz nada (útil em condicionais)
def processar(dados, debug=False):
ctx = MedidorTempo("debug") if debug else nullcontext()
with ctx:
return [x * 2 for x in dados]
ExitStack: context managers dinâmicos
ExitStack permite gerenciar múltiplos context managers de forma dinâmica, especialmente útil quando o número de recursos não é conhecido em tempo de compilação:
from contextlib import ExitStack
def processar_arquivos(caminhos):
"""Abre múltiplos arquivos e processa todos de uma vez."""
with ExitStack() as stack:
arquivos = [
stack.enter_context(open(caminho, 'r', encoding='utf-8'))
for caminho in caminhos
]
# Todos os arquivos são fechados ao sair do bloco
return [f.read() for f in arquivos]
# Outra aplicação: adicionar cleanup condicional
def conexao_com_fallback(usar_pool):
stack = ExitStack()
if usar_pool:
pool = stack.enter_context(ConexaoPool())
conn = pool.obter()
else:
conn = stack.enter_context(ConexaoSimples())
return stack, conn
# Registro de callbacks de cleanup
with ExitStack() as stack:
stack.callback(print, "Cleanup 1 executado")
stack.callback(print, "Cleanup 2 executado")
print("Dentro do bloco")
# Saída:
# Dentro do bloco
# Cleanup 2 executado (LIFO)
# Cleanup 1 executado
Context managers em testes
Context managers são amplamente usados em testes:
import unittest
from unittest.mock import patch, MagicMock
from contextlib import contextmanager
# pytest.raises — verifica que uma exceção é levantada
import pytest
def dividir(a, b):
if b == 0:
raise ZeroDivisionError("Divisão por zero")
return a / b
def test_divisao_por_zero():
with pytest.raises(ZeroDivisionError, match="Divisão por zero"):
dividir(10, 0)
# unittest.mock.patch — substitui objetos durante o teste
def test_chamada_api():
with patch('requests.get') as mock_get:
mock_get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"}
)
# Código que usa requests.get será interceptado
resultado = chamar_api("https://exemplo.com")
mock_get.assert_called_once_with("https://exemplo.com")
@contextmanager
def banco_em_memoria():
"""Context manager para testes com banco de dados em memória."""
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute("CREATE TABLE usuarios (id INTEGER, nome TEXT)")
try:
yield conn
finally:
conn.close()
def test_inserir_usuario():
with banco_em_memoria() as db:
db.execute("INSERT INTO usuarios VALUES (1, 'Ana')")
cursor = db.execute("SELECT nome FROM usuarios WHERE id=1")
assert cursor.fetchone()[0] == 'Ana'
Padrões reais de uso
Gerenciamento de locks
import threading
from contextlib import contextmanager
@contextmanager
def lock_timeout(lock, timeout=5.0):
"""Lock com timeout — evita deadlocks infinitos."""
adquirido = lock.acquire(timeout=timeout)
if not adquirido:
raise TimeoutError(f"Não foi possível adquirir o lock em {timeout}s")
try:
yield
finally:
lock.release()
_lock = threading.Lock()
def operacao_critica():
with lock_timeout(_lock, timeout=2.0):
# Seção crítica protegida
pass
Mudança temporária de estado
@contextmanager
def variavel_de_ambiente(nome, valor):
"""Altera uma variável de ambiente temporariamente."""
valor_original = os.environ.get(nome)
os.environ[nome] = valor
try:
yield
finally:
if valor_original is None:
del os.environ[nome]
else:
os.environ[nome] = valor_original
@contextmanager
def diretorio_atual(caminho):
"""Muda o diretório de trabalho temporariamente."""
anterior = os.getcwd()
os.chdir(caminho)
try:
yield caminho
finally:
os.chdir(anterior)
with variavel_de_ambiente("DEBUG", "true"):
print(os.environ["DEBUG"]) # true
# DEBUG voltou ao valor original
Context managers aninhados
# Múltiplos context managers na mesma linha (Python 3.1+)
with open('entrada.txt') as entrada, open('saida.txt', 'w') as saida:
for linha in entrada:
saida.write(linha.upper())
# Python 3.10+: parênteses para múltiplas linhas
with (
open('log.txt', 'a') as log,
ConexaoBanco("localhost") as db,
MedidorTempo("importacao") as timer,
):
dados = db.buscar_todos()
log.write(f"Buscados {len(dados)} registros\n")
Erros comuns
O erro mais frequente é levantar exceções dentro de __enter__ sem cleanup adequado. Outro problema comum é retornar True em __exit__ acidentalmente (suprimindo exceções sem intenção). No @contextmanager, esquecer o try/finally ao redor do yield faz com que exceções no bloco with sejam ignoradas e o cleanup não execute:
# ERRADO — sem try/finally, exceções pulam o cleanup
@contextmanager
def arquivo_temporario():
f = open('temp.txt', 'w')
yield f
f.close() # Não executa se houver exceção!
# CORRETO — try/finally garante cleanup sempre
@contextmanager
def arquivo_temporario():
f = open('temp.txt', 'w')
try:
yield f
finally:
f.close() # Sempre executa
Quando usar context managers?
Use context managers sempre que houver setup e teardown: abrir/fechar recursos, iniciar/finalizar transações, adquirir/liberar locks, salvar/restaurar estado. Se você se pega escrevendo try/finally para cleanup de recursos, provavelmente deveria estar usando um context manager.
Termos Relacionados
- Decorators - @contextmanager é um decorator
- Classe - Context managers podem ser classes
- Exceções - exit recebe info de exceções
- Iterator - Outro protocolo do Python