Módulo 6 - Context managers e o with

Casos reais: arquivos, locks, transações e cronômetro

10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026

O que você vai aprender

  • Reconhecer arquivos e locks como context managers prontos.
  • Entender por que with é essencial para liberar um lock sempre.
  • Montar um gerenciador de transação com commit e rollback.
  • Criar um cronômetro de bloco com @contextmanager.

Arquivos e locks já vêm prontos

Dois recursos essenciais já chegam prontos para o with. O primeiro é o arquivo, que você abre com with open e não precisa fechar à mão. O segundo é o lock de threads, usado quando várias threads podem mexer no mesmo dado e você precisa garantir que só uma faça isso por vez. O lock precisa ser liberado sempre, e é exatamente aí que o with brilha: se o trecho protegido lançar uma exceção, o lock ainda é liberado, evitando que o programa inteiro trave à espera de um bloqueio que ninguém solta.

import threading

contador = 0
trava = threading.Lock()

def incrementar():
    global contador
    with trava:            # adquire o lock ao entrar
        atual = contador
        contador = atual + 1
    # o lock e liberado ao sair, mesmo se algo falhar dentro

# Sem o with, uma excecao no trecho protegido deixaria a trava
# fechada para sempre, travando as demais threads.

with trava garante que o lock seja liberado, mesmo se o trecho protegido falhar.

Repare que você não chama trava.acquire nem trava.release à mão. O with cuida dos dois: adquire ao entrar e libera ao sair. Esse é o motivo de o with ser a forma recomendada de usar locks. Liberar um lock à mão é a receita para o dia em que uma exceção pula o release e o programa congela, um bug angustiante de diagnosticar. A garantia de limpeza deixa de ser conveniência e vira requisito de correção.

Transação: confirmar ou desfazer

Um caso onde o context manager mostra todo o seu valor é a transação. A regra é: se o bloco terminou sem erro, confirme as alterações (commit); se houve uma exceção, desfaça tudo (rollback) para não deixar o banco em estado inconsistente. Essa lógica se encaixa perfeitamente no __exit__, que sabe se houve exceção pelos seus parâmetros. O gerenciador abaixo simula um banco para ilustrar; a estrutura é a mesma que bibliotecas reais adotam.

from contextlib import contextmanager

class Banco:
    def __init__(self):
        self.pendentes = []
    def gravar(self, item):
        self.pendentes.append(item)
    def commit(self):
        print(f"commit: {self.pendentes}")
        self.pendentes = []
    def rollback(self):
        print(f"rollback: descartando {self.pendentes}")
        self.pendentes = []

@contextmanager
def transacao(banco):
    try:
        yield banco
        banco.commit()          # sem erro: confirma
    except Exception:
        banco.rollback()        # com erro: desfaz
        raise                   # e deixa o erro subir

banco = Banco()
try:
    with transacao(banco) as b:
        b.gravar("linha 1")
        raise ValueError("deu ruim")
except ValueError:
    print("erro tratado")
# rollback: descartando ['linha 1'] / erro tratado

commit quando o bloco termina bem; rollback e re-raise quando ocorre uma exceção.

Um cronômetro de bloco

Para fechar, um gerenciador simples e muito útil: um cronômetro que mede quanto tempo um bloco levou. Ele encaixa no molde do @contextmanager como uma luva. Antes do yield, você marca o instante inicial; depois do yield, calcula e mostra a duração. Como o pós-yield fica no finally, a medição acontece mesmo que o bloco lance uma exceção, o que é ótimo para descobrir se uma operação que falhou também estava lenta. É o mesmo espírito do decorador de cronômetro do Módulo 5, agora aplicado a um trecho arbitrário de código.

from contextlib import contextmanager
import time

@contextmanager
def cronometro(rotulo):
    inicio = time.perf_counter()
    try:
        yield
    finally:
        duracao = time.perf_counter() - inicio
        print(f"{rotulo}: {duracao:.4f}s")

with cronometro("processamento"):
    total = sum(i * i for i in range(1_000_000))
# processamento: 0.0xxx s

Cronômetro de bloco: mede o tempo do trecho, mesmo que ele lance exceção.

Com esses casos, o quadro fica completo. Arquivos e locks vêm prontos; transações e cronômetros você monta com o protocolo e o atalho do yield. O fio comum é sempre o mesmo: um recurso ou uma ação tem começo e fim, e o with garante que o fim aconteça, aconteça o que acontecer no meio. Levar esse hábito para o seu código, usar with com tudo que o suporta e criar gerenciadores para os seus próprios recursos, é uma marca de código robusto e profissional.

Teste rápido

Por que usar with em um lock de threads é considerado essencial, e não só conveniente?

Perguntas frequentes

Por que não liberar o lock à mão com acquire e release?
Porque é frágil. Se uma exceção acontecer entre o acquire e o release, a linha do release não roda e o lock fica preso, travando todas as threads que dependem dele. O with adquire e libera automaticamente, garantindo a liberação mesmo com erro. Por isso a forma recomendada de usar um lock é sempre with trava.
No gerenciador de transação, por que dar raise depois do rollback?
Porque desfazer as alterações não faz o problema desaparecer. Quem chamou a transação precisa saber que ela falhou para tratar a situação, avisar o usuário, tentar de novo, registrar o erro. O rollback cuida da consistência do banco; o raise cuida de propagar a falha. Esconder a exceção depois do rollback criaria bugs silenciosos.
O cronômetro mede o tempo mesmo se o bloco der erro?
Sim, porque a medição fica no finally, depois do yield. O finally roda na saída do bloco aconteça o que acontecer, inclusive quando uma exceção está subindo. Isso é útil justamente para descobrir se uma operação que falhou também estava demorando, um sinal que ajuda no diagnóstico.
Bibliotecas de banco reais usam esse padrão de transação?
Usam, sim. Muitas conexões de banco de dados oferecem suporte a with para justamente confirmar ou desfazer a transação conforme o bloco termine bem ou com erro. O exemplo do curso simula um banco para deixar a lógica visível, mas a estrutura, commit no sucesso e rollback no erro, é a mesma que você encontra nas bibliotecas de produção.
Posso combinar vários desses context managers de uma vez?
Pode. Você aninha ou separa por vírgula quando o número é fixo, e usa o ExitStack quando é variável. Por exemplo, abrir um arquivo dentro de uma transação, tudo cronometrado. Como cada gerenciador garante a própria limpeza, combiná-los mantém a robustez: cada recurso é liberado na saída, na ordem correta.

Fontes

Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.