Python Gerenciadores de Contexto (with)
Desde a aula Abrindo Arquivos, você abre arquivos com with open(…) as arquivo: e confia que o Python vai fechá-los no fim. Mas quem fecha? E como o with sabe o que fazer com um arquivo? Nesta aula você vai ver o with por dentro: os dois métodos especiais que ele chama, __enter__ e __exit__, e o atalho do módulo contextlib para escrever os seus com um gerador.
- O que é um gerenciador de contexto e o problema que ele resolve.
- O que o
withfaz por dentro, e por que ele é umtry/finallydisfarçado. - A criar o seu com
__enter__e__exit__, e o que oasrecebe. - O que o
__exit__recebe quando dá erro, e como (e quando não) engolir a exceção. - A escrever um gerenciador com
@contextmanager,yieldetry/finally. suppresse vários gerenciadores no mesmowith.- Os erros reais:
does not support the context manager protocol,I/O operation on closed file,generator didn't yielde a limpeza que não acontece.
A ideia: a luz do quarto
Ao entrar no quarto, você acende a luz; ao sair, apaga. Tem gente que sempre esquece de apagar, principalmente quando sai com pressa (o telefone tocou, a panela ferveu). Uma solução é o sensor de presença: ele acende quando você entra e apaga quando você sai, não importa como você saiu.
Muita coisa em programação tem esse formato "preparar, usar, arrumar": abrir e fechar um arquivo, conectar e desconectar de um banco de dados, travar e destravar um recurso. O gerenciador de contexto é o sensor de presença: o with marca o "quarto" (o bloco indentado), e o gerenciador cuida de preparar na entrada e de arrumar na saída, até quando a saída é por um erro.
| Momento | Na luz do quarto | No arquivo | Método chamado |
|---|---|---|---|
Entrar no bloco with | Acende a luz | Abre o arquivo | __enter__ |
| Dentro do bloco | Você lê o livro | Você lê e escreve | (o seu código) |
| Sair do bloco (normal ou por erro) | Apaga a luz | Fecha o arquivo | __exit__ |
1. O with que você já conhece
O atributo closed ("fechado") de um arquivo mostra se ele já foi fechado. Olhe o valor dentro e fora do with:
with open("compras.txt", "w", encoding="utf-8") as arquivo:
arquivo.write("pão\nleite\n")
print("Dentro do with, fechado?", arquivo.closed)
print("Depois do with, fechado?", arquivo.closed)
$ python3 fechado.py Dentro do with, fechado? False Depois do with, fechado? True
Ninguém escreveu arquivo.close(), e mesmo assim, ao sair do bloco, o arquivo estava fechado. Repare também que a variável arquivo continua existindo depois do with: o que acabou foi o "contexto", não a variável.
O jeito antigo: try e finally
Sem o with, para garantir o fechamento mesmo quando algo dá errado, é preciso um try/finally (aula else e finally). Este programa erra de propósito na linha 4: tenta gravar um número, e write só aceita texto.
arquivo = open("compras.txt", "w", encoding="utf-8")
try:
arquivo.write("pão\n")
arquivo.write(10)
finally:
arquivo.close()
print("Fechado?", arquivo.closed)
$ python3 sem_with.py
Fechado? True
Traceback (most recent call last):
File "/home/ana/sem_with.py", line 4, in <module>
arquivo.write(10)
~~~~~~~~~~~~~^^^^
TypeError: write() argument must be str, not int
O finally rodou antes de o erro aparecer: primeiro o Python arrumou a casa (Fechado? True), depois mostrou o traceback e parou. É exatamente isso que o with faz por você, em uma linha em vez de cinco.
2. O seu gerenciador: __enter__ e __exit__
Qualquer objeto pode ir num with, desde que tenha dois métodos especiais (aula Métodos Especiais). A padaria da Ana abre e fecha todo dia; um gerenciador de contexto garante que ela nunca fique aberta:
class Expediente:
def __init__(self, loja):
self.loja = loja
def __enter__(self):
print(f"Abrindo a {self.loja}.")
return self
def __exit__(self, tipo, valor, rastro):
print(f"Fechando a {self.loja}.")
return False
def vender(self, produto):
print(f" Vendido: {produto}")
with Expediente("padaria") as dia:
dia.vender("pão")
dia.vender("café")
print("Fim do dia.")
$ python3 loja.py Abrindo a padaria. Vendido: pão Vendido: café Fechando a padaria. Fim do dia.
- with Expediente("padaria")
- Cria o objeto, como sempre, e entrega para o
with. - def __enter__(self):
- "Entrar". O
withchama este método antes da primeira linha do bloco. É o "preparar". - return self
- O que o
__enter__devolve vai para a variável doas. Aqui, o próprio objeto: por issodia.vender(…)funciona. (Oopen()também devolve o próprio arquivo.) - as dia
- O nome que recebe o retorno do
__enter__. É opcional:with Expediente("padaria"):também vale, se você não precisar dele. - def __exit__(self, tipo, valor, rastro):
- "Sair". O
withchama este método quando o bloco acaba, de qualquer jeito. É o "arrumar". Os três parâmetros contam se houve erro (seção 3). - return False
- "Não engoli nenhum erro": se uma exceção aconteceu no bloco, ela continua subindo. Na seção 3 você vê o
True.
O with por dentro
O with do loja.py faz, escondido, mais ou menos isto:
class Expediente:
def __init__(self, loja):
self.loja = loja
def __enter__(self):
print(f"Abrindo a {self.loja}.")
return self
def __exit__(self, tipo, valor, rastro):
print(f"Fechando a {self.loja}.")
return False
gerente = Expediente("padaria")
dia = gerente.__enter__()
try:
print(" vendendo...")
finally:
gerente.__exit__(None, None, None)
$ python3 por_dentro.py Abrindo a padaria. vendendo... Fechando a padaria.
- Avalia a expressão depois do
withe guarda o gerenciador. - Chama
__enter__()e põe o resultado na variável doas. - Roda o bloco dentro de um
try. - No fim, chama
__exit__: comNone, None, Nonese tudo correu bem; com os dados da exceção se deu erro. (Owithde verdade ainda decide, pelo retorno do__exit__, se a exceção continua.)
Missão: a classe Luz só tem o __enter__. Rode e leia o erro. Depois escreva o __exit__(self, tipo, valor, rastro), que mostra Luz apagada. e devolve False.
class Luz:
def __enter__(self):
print("Luz acesa.")
return self
with Luz():
print("Lendo o livro...")
class Luz:
def __enter__(self):
print("Luz acesa.")
return self
def __exit__(self, tipo, valor, rastro):
print("Luz apagada.")
return False
with Luz():
print("Lendo o livro...")
3. Quando dá erro dentro do bloco
A promessa do with é: o __exit__ roda sempre. Vamos testar com uma divisão por zero no meio do expediente. Este __exit__ também mostra o que recebeu:
class Expediente:
def __init__(self, loja):
self.loja = loja
def __enter__(self):
print(f"Abrindo a {self.loja}.")
return self
def __exit__(self, tipo, valor, rastro):
print(f"Fechando a {self.loja}.")
print(f" (tipo={tipo}, valor={valor!r})")
return False
def vender(self, produto):
print(f" Vendido: {produto}")
with Expediente("padaria") as dia:
dia.vender("pão")
troco = 10 / 0
dia.vender("café")
print("Fim do dia.")
$ python3 loja_erro.py
Abrindo a padaria.
Vendido: pão
Fechando a padaria.
(tipo=<class 'ZeroDivisionError'>, valor=ZeroDivisionError('division by zero'))
Traceback (most recent call last):
File "/home/ana/loja_erro.py", line 20, in <module>
troco = 10 / 0
~~~^~~
ZeroDivisionError: division by zero
- O
dia.vender("café")não rodou: o erro da linha 20 interrompeu o bloco. - Mas a padaria fechou: o
__exit__rodou antes de o traceback aparecer. - O
__exit__recebeu o tipo da exceção (ZeroDivisionError), o valor (a exceção em si, com a mensagem) e o rastro (o traceback, um objeto que aqui não mostramos). O!rna f-string mostra o valor como o Python o escreveria no código. - Como o
__exit__devolveuFalse, a exceção seguiu o caminho normal: traceback, programa parado, e oFim do dia.nunca apareceu.
Engolindo a exceção: return True
Se o __exit__ devolver True, o with entende "eu cuidei do erro" e a exceção some. O programa segue depois do bloco:
class IgnorarDivisaoPorZero:
def __enter__(self):
return self
def __exit__(self, tipo, valor, rastro):
if tipo is ZeroDivisionError:
print("Divisão por zero ignorada.")
return True
return False
with IgnorarDivisaoPorZero():
print("Antes da conta")
media = 10 / 0
print("Esta linha não roda")
print("O programa continuou.")
$ python3 engolir.py Antes da conta Divisão por zero ignorada. O programa continuou.
Repare no if tipo is ZeroDivisionError: o gerenciador só engole aquele erro. Um __exit__ que sempre devolve True esconde todos os erros do bloco, inclusive os que você não imaginou (um nome digitado errado, um TypeError), e o programa segue com dados errados sem avisar ninguém. Na dúvida, devolva False. E lembre: a linha que deu erro e as seguintes, dentro do bloco, não rodam (Esta linha não roda).
4. O atalho: @contextmanager
Escrever uma classe com dois métodos para "abrir e fechar a padaria" é muita cerimônia. O módulo contextlib tem o decorador @contextmanager (aula Decoradores) que transforma um gerador (aula Geradores) em gerenciador de contexto:
from contextlib import contextmanager
@contextmanager
def expediente(loja):
print(f"Abrindo a {loja}.")
try:
yield loja.upper()
finally:
print(f"Fechando a {loja}.")
with expediente("padaria") as placa:
print(" Placa na porta:", placa)
print("---")
with expediente("farmácia"):
print(" Vendendo...")
raise ValueError("faltou troco")
$ python3 gerador.py
Abrindo a padaria.
Placa na porta: PADARIA
Fechando a padaria.
---
Abrindo a farmácia.
Vendendo...
Fechando a farmácia.
Traceback (most recent call last):
File "/home/ana/gerador.py", line 20, in <module>
raise ValueError("faltou troco")
ValueError: faltou troco
- @contextmanager
- Escreve o
__enter__e o__exit__para você, a partir do gerador. - o que vem antes do yield
- É o
__enter__: roda na entrada dowith. - yield loja.upper()
- O ponto em que o bloco do
withroda. O valor entregue peloyieldvai para oas(aqui,"PADARIA"). Umyieldsozinho entregaNone. - try: … finally:
- Se o bloco do
withder erro, a exceção "aparece" dentro do gerador, na linha doyield. Sem otry/finally, a limpeza seria pulada (veja em Quando dá errado). - o que vem depois do yield (no finally)
- É o
__exit__: roda na saída dowith, de qualquer jeito.
Na farmácia, o raise (aula raise) lançou um erro dentro do bloco: o finally fechou a farmácia, e o erro seguiu até o traceback.
5. Prontos para usar, e vários juntos
suppress: ignorar um erro esperado
O contextlib.suppress ("suprimir") é um gerenciador pronto que engole os tipos de exceção que você disser, como o IgnorarDivisaoPorZero da seção 3. Útil para "apague o rascunho, se ele existir":
from contextlib import suppress
import os
with suppress(FileNotFoundError):
os.remove("rascunho.txt")
print("Apaguei o rascunho.")
print("Seguindo em frente.")
$ python3 suppress.py Seguindo em frente. « não havia rascunho: o erro foi engolido $ echo "rascunho" > rascunho.txt $ python3 suppress.py Apaguei o rascunho. Seguindo em frente.
Na primeira vez, o os.remove lançou FileNotFoundError, o suppress engoliu, e a linha Apaguei o rascunho. foi pulada. É o mesmo que um try com except FileNotFoundError: pass, em menos linhas.
Dois arquivos no mesmo with
Para copiar de um arquivo para outro, os dois precisam estar abertos ao mesmo tempo. Um with aceita vários gerenciadores, separados por vírgula; entre parênteses, cada um fica numa linha:
with open("entrada.txt", "w", encoding="utf-8") as f:
f.write("pão\nleite\ncafé\n")
with (
open("entrada.txt", encoding="utf-8") as origem,
open("saida.txt", "w", encoding="utf-8") as destino,
):
for linha in origem:
destino.write(linha.upper())
with open("saida.txt", encoding="utf-8") as f:
print(f.read(), end="")
$ python3 dois.py PÃO LEITE CAFÉ
Os dois são abertos na ordem em que aparecem e fechados na ordem contrária, ao fim do bloco. A forma com parênteses existe desde o Python 3.10; num Python mais antigo, escreva tudo numa linha só: with open(…) as origem, open(…) as destino:.
Formas diferentes de garantir a limpeza
| Forma | Quando usar |
|---|---|
try / finally na mão | Uma limpeza que aparece uma vez só no programa |
with com um gerenciador pronto | Sempre que existe um: open, suppress, conexões de banco (aula sqlite3) |
@contextmanager com yield | O seu "preparar e arrumar" de todo dia: curto, e lê de cima para baixo |
Classe com __enter__ e __exit__ | Quando o gerenciador é um objeto com outros métodos (como o dia.vender), ou precisa decidir com cuidado que exceções engolir |
Quando dá errado
class Expediente:
def __init__(self, loja):
self.loja = loja
with Expediente("padaria") as dia:
print("vendendo")
$ python3 erro_protocolo.py
Traceback (most recent call last):
File "/home/ana/erro_protocolo.py", line 6, in <module>
with Expediente("padaria") as dia:
~~~~~~~~~~^^^^^^^^^^^
TypeError: 'Expediente' object does not support the context manager protocol (missed __exit__ method)
"O objeto Expediente não segue o protocolo de gerenciador de contexto (faltou o método __exit__)". Protocolo aqui quer dizer "o combinado": ter __enter__ e __exit__. Esta classe não tem nenhum dos dois, mas a mensagem cita só o __exit__. Na classe que só tem o __exit__, o fim da mensagem muda para (missed __enter__ method). Como consertar: escreva os dois métodos, ou use @contextmanager.
with
with open("compras.txt", "w", encoding="utf-8") as arquivo:
arquivo.write("pão\n")
arquivo.write("leite\n")
$ python3 erro_fechado.py
Traceback (most recent call last):
File "/home/ana/erro_fechado.py", line 4, in <module>
arquivo.write("leite\n")
~~~~~~~~~~~~~^^^^^^^^^^^
ValueError: I/O operation on closed file.
"Operação de entrada/saída num arquivo fechado". A linha 4 está fora do bloco (sem indentação), e lá o arquivo já foi fechado pelo __exit__. Como consertar: indente a linha para dentro do with, ou abra o arquivo de novo (com "a", de append, para acrescentar no fim).
__enter__ esqueceu o return self
O Expediente do loja.py, com o __enter__ sem a linha return self:
$ python3 erro_enter_none.py
Abrindo a padaria.
Fechando a padaria.
Traceback (most recent call last):
File "/home/ana/erro_enter_none.py", line 17, in <module>
dia.vender("pão")
^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'vender'
"O objeto NoneType não tem o atributo vender". Sem return, o __enter__ devolveu None, e foi o None que o as dia recebeu. O dia.vender falhou na linha 17, e mesmo assim a padaria fechou. Como consertar: return self no fim do __enter__.
@contextmanager sem try/finally: a limpeza não acontece
from contextlib import contextmanager
@contextmanager
def expediente(loja):
print(f"Abrindo a {loja}.")
yield
print(f"Fechando a {loja}.")
with expediente("farmácia"):
print(" Vendendo...")
raise ValueError("faltou troco")
$ python3 gerador_sem_finally.py
Abrindo a farmácia.
Vendendo...
Traceback (most recent call last):
File "/home/ana/gerador_sem_finally.py", line 13, in <module>
raise ValueError("faltou troco")
ValueError: faltou troco
Faltou o Fechando a farmácia.! O erro do bloco foi lançado dentro do gerador, na linha do yield, e interrompeu a função ali mesmo: o print de depois nunca rodou. Sem erro, funcionaria; é justamente no dia do erro que a farmácia fica aberta. Como consertar: ponha o yield dentro de um try e a limpeza no finally, como no gerador.py.
yield
from contextlib import contextmanager
@contextmanager
def expediente(loja):
print(f"Abrindo a {loja}.")
if loja == "padaria":
yield
print(f"Fechando a {loja}.")
with expediente("farmácia"):
print(" Vendendo...")
$ python3 erro_sem_yield.py
Abrindo a farmácia.
Fechando a farmácia.
Traceback (most recent call last):
File "/home/ana/erro_sem_yield.py", line 12, in <module>
with expediente("farmácia"):
~~~~~~~~~~^^^^^^^^^^^^
File "/usr/lib/python3.14/contextlib.py", line 143, in __enter__
raise RuntimeError("generator didn't yield") from None
RuntimeError: generator didn't yield
"O gerador não fez yield". Para a farmácia, o if pulou o yield, e a função terminou sem entregar o ponto em que o bloco deveria rodar. O traceback tem um segundo trecho, dentro do contextlib.py: é o __enter__ que o @contextmanager escreveu para você. O Vendendo... não apareceu: o bloco nem começou. Com dois yield seguidos, o erro é o contrário: RuntimeError: generator didn't stop ("o gerador não parou"). Como consertar: o gerador de um @contextmanager tem de passar por exatamente um yield, sempre.
Referência rápida
| Escrita | O que faz |
|---|---|
with gerenciador as nome: | Chama __enter__, guarda o retorno em nome, roda o bloco e chama __exit__ no fim, sempre |
def __enter__(self): … return self | O "preparar"; o retorno vai para o as |
def __exit__(self, tipo, valor, rastro): | O "arrumar"; recebe None, None, None ou os dados da exceção |
return False / return True no __exit__ | Deixa a exceção seguir / engole a exceção |
@contextmanager + try: yield + finally: | Gerenciador a partir de um gerador (de contextlib) |
with suppress(Erro): | Engole as exceções do tipo Erro (de contextlib) |
with (a as x, b as y): | Vários gerenciadores; fecham na ordem contrária (3.10+) |
| Sintoma | Causa provável |
|---|---|
does not support the context manager protocol (missed __exit__ method) | Faltam o __enter__ e/ou o __exit__ |
ValueError: I/O operation on closed file. | Usou o arquivo fora do bloco with |
AttributeError: 'NoneType' object has no attribute … no as | O __enter__ não devolve nada |
| A limpeza não roda quando dá erro | @contextmanager sem try/finally em volta do yield |
RuntimeError: generator didn't yield / didn't stop | O gerador passou por zero / por mais de um yield |
| Um erro "sumiu" sem aviso | O __exit__ devolve True sempre |
Exercícios
1. Os dois métodos
Escreva os nomes dos dois métodos especiais que o with chama, na ordem:
2. O que vai para o as
Complete o __enter__ para que with Expediente("padaria") as dia: receba o próprio objeto:
3. Ignorando um erro
Complete para apagar velho.txt sem dar erro se ele não existir:
os.remove("velho.txt")
4. Uma exceção acontece na segunda linha de um bloco with, e o __exit__ devolve False. O que acontece?
5. Num @contextmanager, onde fica o código que deve rodar mesmo se der erro no bloco do with?
6. Por que um __exit__ que sempre devolve True é perigoso?
Missão final: o caixa sempre fecha
Missão: num dia sem clientes, a conta da média divide por zero, e o caixa fica aberto: o Caixa fechado. não aparece. Proteja a limpeza: ponha o yield dentro de um try: e o print("Caixa fechado.") num finally:. O erro vai continuar aparecendo (ele é de verdade), mas agora depois de o caixa fechar.
from contextlib import contextmanager
@contextmanager
def caixa():
print("Caixa aberto.")
yield
print("Caixa fechado.")
with caixa():
clientes = 0
print("Média por cliente:", 120 / clientes)
from contextlib import contextmanager
@contextmanager
def caixa():
print("Caixa aberto.")
try:
yield
finally:
print("Caixa fechado.")
with caixa():
clientes = 0
print("Média por cliente:", 120 / clientes)
Com o finally, a saída fica Caixa aberto., Caixa fechado. e, por último, o traceback do ZeroDivisionError. Para tratar o erro de verdade (mostrar "sem clientes hoje" em vez do traceback), use um if clientes == 0 ou um try/except dentro do bloco.
Resumo
- Um gerenciador de contexto cuida do "preparar e arrumar" em volta de um bloco: o
withgarante a arrumação, até quando o bloco termina por erro. - O
withchama__enter__na entrada (o retorno vai para oas) e__exit__na saída. Por dentro, é umtry/finally. - O
__exit__recebe tipo, valor e rastro da exceção (ou trêsNone). DevolvendoTrue, engole a exceção; faça isso só com o erro que você esperava. @contextmanagerfaz um gerenciador a partir de um gerador: antes doyieldé a entrada, ofinallyé a saída. Sem otry/finally, a limpeza é pulada quando dá erro.suppress(Erro)engole um erro esperado; umwithaceita vários gerenciadores, que fecham na ordem contrária.