Context managers
Nesta aula, você aprenderá sobre context managers em Python: o que são, como usar a declaração `with`, como implementar o protocolo com `__enter__` e `__exit__`, e como criar seus próprios context managers usando classes ou o módulo `contextlib`. Inclui exemplos práticos, boas práticas e exercícios para fixar o conteúdo.
Context managers são uma das funcionalidades mais elegantes do Python, permitindo gerenciar recursos (como arquivos, conexões de rede, locks) de forma segura e concisa. Eles garantem que a limpeza de recursos seja feita automaticamente, mesmo em caso de exceções, evitando vazamentos e tornando o código mais legível. Nesta aula, vamos explorar o protocolo por trás deles, a declaração with, o módulo contextlib e como criar seus próprios context managers.
Dominar context managers é essencial para escrever código Python idiomático e robusto. Você os encontrará em bibliotecas padrão e de terceiros, e ao criar seus próprios, poderá encapsular padrões de configuração e limpeza de forma reutilizável.
O protocolo (__enter__/__exit__)
Um context manager é um objeto que implementa dois métodos especiais: __enter__ e __exit__. O método __enter__ é chamado quando o bloco with é iniciado, e seu valor de retorno é atribuído à variável após o as. O método __exit__ é chamado quando o bloco é encerrado, seja normalmente ou devido a uma exceção. Ele recebe três argumentos: tipo da exceção, valor e traceback. Se retornar True, a exceção é suprimida; se retornar False (ou nada), a exceção é propagada.
Vamos ver um exemplo simples de um context manager que controla o acesso a um recurso:
class Recurso:
def __enter__(self):
print("Abrindo recurso")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Fechando recurso")
# Retorna False para permitir exceções, se houver
return False
with Recurso() as r:
print("Dentro do bloco")
# Saída:
# Abrindo recurso
# Dentro do bloco
# Fechando recursoO método __exit__ é crucial para garantir a limpeza mesmo quando ocorre uma exceção. Por exemplo:
class Recurso:
def __enter__(self):
print("Abrindo")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Fechando")
if exc_type is not None:
print(f"Exceção capturada: {exc_val}")
return False
with Recurso() as r:
raise ValueError("Algo deu errado")
# Abrindo
# Fechando
# Exceção capturada: Algo deu errado
# Traceback (most recent call last): ...Note que a exceção é propagada após o __exit__ porque retornamos False. Se quiséssemos suprimir a exceção, retornaríamos True.
with
A declaração with é a forma mais comum de usar um context manager. Ela garante que o método __exit__ seja chamado automaticamente. A sintaxe básica é:
with EXPRESSÃO as VARIÁVEL:
BLOCOO exemplo mais clássico é o gerenciamento de arquivos. Antes, era comum usar try/finally para garantir o fechamento:
arquivo = open('exemplo.txt', 'w')
try:
arquivo.write('Olá, mundo!')
finally:
arquivo.close()Com with, fica muito mais limpo:
with open('exemplo.txt', 'w') as arquivo:
arquivo.write('Olá, mundo!')Além de arquivos, with é usado com locks, conexões de banco de dados, e qualquer recurso que precise de liberação. Também é possível utilizar múltiplos context managers em um único with:
with open('a.txt') as a, open('b.txt') as b:
# processa a e bE, desde o Python 3.10, você pode usar parênteses para melhorar a legibilidade:
with (
open('a.txt') as a,
open('b.txt') as b
):
# processa a e bcontextlib
O módulo contextlib fornece utilitários para criar e trabalhar com context managers. Os principais são:
contextlib.contextmanager: um decorador que transforma uma função geradora em um context manager.contextlib.closing: um context manager que chama o métodoclose()do objeto.contextlib.suppress: suprime exceções específicas.contextlib.redirect_stdouteredirect_stderr: redirecionam a saída padrão ou de erro.contextlib.ExitStack: gerencia dinamicamente múltiplos context managers.
O @contextmanager é extremamente útil para criar context managers sem escrever uma classe completa. Você define uma função geradora com um yield no ponto onde o bloco with é executado. O código antes do yield corresponde ao __enter__, e o código após o yield ao __exit__. Exemplo:
from contextlib import contextmanager
@contextmanager
def gerenciar_recurso():
print("Abrindo")
try:
yield "recurso"
finally:
print("Fechando")
with gerenciar_recurso() as r:
print(f"Usando {r}")
# Abrindo
# Usando recurso
# FechandoO yield pode ser usado em um try/finally para garantir a limpeza mesmo se ocorrer uma exceção dentro do bloco.
Outro utilitário interessante é o suppress, que suprime exceções específicas sem precisar de um bloco try/except:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('arquivo_inexistente.txt')O ExitStack permite gerenciar um número dinâmico de context managers, útil em situações onde você não sabe quantos recursos serão necessários:
from contextlib import ExitStack
with ExitStack() as stack:
files = [stack.enter_context(open(f'arquivo_{i}.txt', 'w')) for i in range(3)]
# Todos os arquivos serão fechados ao sair do withCriando os seus
Existem duas formas principais de criar seus próprios context managers: usando classes com os métodos __enter__ e __exit__, ou usando o decorador @contextmanager com funções geradoras. A escolha depende da complexidade e da necessidade de armazenar estado.
Abordagem com classe: É mais adequada quando você precisa de um objeto que mantenha estado durante o bloco, ou quando a lógica de entrada/saída é complexa. Exemplo de um context manager que muda o diretório atual:
import os
class mudar_diretorio:
def __init__(self, novo_caminho):
self.novo_caminho = novo_caminho
self.caminho_original = None
def __enter__(self):
self.caminho_original = os.getcwd()
os.chdir(self.novo_caminho)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.caminho_original)
return False
# Uso:
with mudar_diretorio('/tmp'):
print(os.getcwd()) # /tmp
print(os.getcwd()) # diretório originalAbordagem com generador: É mais concisa para casos simples. Exemplo de um context manager que mede o tempo de execução:
import time
from contextlib import contextmanager
@contextmanager
def temporizador():
inicio = time.time()
try:
yield
finally:
fim = time.time()
print(f"Tempo decorrido: {fim - inicio:.2f} segundos")
with temporizador():
# Simula um trabalho
time.sleep(1)Ao criar seus próprios context managers, é importante seguir boas práticas: sempre garantir a liberação de recursos no __exit__ ou após o yield (usando finally), tratar exceções adequadamente e documentar o comportamento.
Boas práticas e observações finais
- Sempre feche recursos explicitamente em context managers que você criar.
- Use
@contextmanagerpara context managers simples e classes para os mais complexos. - Se
__exit__retornarTrue, a exceção é suprimida; use isso com cuidado para não esconder erros. - Evite usar context managers para lógica que não envolva recursos, pois isso pode confundir.
- Prefira
withatry/finallysempre que possível.
Referências
- Documentação sobre context managers
- Módulo contextlib
- Declaração with na referência
- Tutorial: Ações de limpeza predefinidas
- Real Python: The with Statement
- PEP 343 – The “with” Statement
Exercícios
Implemente um context manager chamado
abrir_arquivoque gerencia a abertura e fechamento de um arquivo, mas que imprima uma mensagem quando o arquivo for aberto e fechado. Use a abordagem de classe.✓ Resposta:class abrir_arquivo: def __init__(self, nome, modo): self.nome = nome self.modo = modo self.arquivo = None def __enter__(self): print(f"Abrindo arquivo {self.nome}") self.arquivo = open(self.nome, self.modo) return self.arquivo def __exit__(self, exc_type, exc_val, exc_tb): print(f"Fechando arquivo {self.nome}") if self.arquivo: self.arquivo.close() return False with abrir_arquivo('exemplo.txt', 'w') as f: f.write('Olá') # Abrindo arquivo exemplo.txt # Fechando arquivo exemplo.txtCrie um context manager usando
@contextmanagerque execute uma função de setup antes do bloco e uma função de teardown depois, mesmo se ocorrer uma exceção.✓ Resposta:from contextlib import contextmanager def setup(): print("Setup executado") def teardown(): print("Teardown executado") @contextmanager def gerenciar(): setup() try: yield finally: teardown() with gerenciar(): print("Dentro do bloco") # Setup executado # Dentro do bloco # Teardown executadoUsando
contextlib.suppress, escreva um trecho de código que tente remover um arquivo que pode não existir, sem gerar exceção.✓ Resposta:import os from contextlib import suppress with suppress(FileNotFoundError): os.remove('arquivo_inexistente.txt') print("Continua sem erro")Explique a diferença entre retornar
TrueeFalseno método__exit__e dê um exemplo onde retornarTruepode ser útil.✓ Resposta:RetornarTruesuprime a exceção, enquantoFalsea propaga. Um exemplo útil é um context manager que ignora certas exceções, como em um sistema de logging onde você não quer interromper o fluxo por erros esperados. Por exemplo:class ignorar_valor_error: def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is ValueError: print("Ignorando ValueError") return True return False with ignorar_valor_error(): raise ValueError("Erro ignorado") print("Fim")Usando
ExitStack, escreva um código que abra três arquivos dinamicamente e garanta que todos sejam fechados ao final.✓ Resposta:from contextlib import ExitStack with ExitStack() as stack: arquivos = [] for i in range(3): f = stack.enter_context(open(f'arquivo_{i}.txt', 'w')) arquivos.append(f) # Ao sair do with, todos os arquivos são fechados automaticamente