Context managers
Nesta aula, você vai dominar os context managers em Python, desde o protocolo com __enter__ e __exit__, passando pela instrução with, até a biblioteca contextlib e a criação de gerenciadores personalizados. Vamos explorar com exemplos práticos e exercícios para consolidar o aprendizado.
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 contextoNo 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 recursoObserve 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
# SaindoOutros 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 sairCriando 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 similarOutro 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 originalAo 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
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: erroUse
contextlib.contextmanagerpara 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 automaticamenteCrie um context manager que altere o diretório de trabalho para um dado caminho e, ao sair, restaure o original. Use a biblioteca
ose 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())Explique a diferença entre retornar
TrueeFalseno método__exit__. Dê um exemplo onde retornarTrueé útil.✓ Resposta: RetornarTruesuprime a exceção, ou seja, ela não é propagada para o chamador. RetornarFalsepermite que a exceção continue a se propagar. Um exemplo útil é um context manager que ignora exceções específicas, comoFileNotFoundError, 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 suprimidaUsando
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