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 recurso

O 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:
    BLOCO

O 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 b

E, 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 b

contextlib

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étodo close() do objeto.
  • contextlib.suppress: suprime exceções específicas.
  • contextlib.redirect_stdout e redirect_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
# Fechando

O 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 with

Criando 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 original

Abordagem 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 @contextmanager para context managers simples e classes para os mais complexos.
  • Se __exit__ retornar True, 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 with a try/finally sempre que possível.

Referências

Exercícios

  1. Implemente um context manager chamado abrir_arquivo que 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.txt
  2. Crie um context manager usando @contextmanager que 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 executado
  3. Usando 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")
  4. Explique a diferença entre retornar True e False no método __exit__ e dê um exemplo onde retornar True pode ser útil.

    ✓ Resposta:Retornar True suprime a exceção, enquanto False a 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")
  5. 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