O with existe para que a limpeza aconteça independentemente do que o corpo faça. Retorne cedo, levante uma exceção ou termine normalmente: o código de saída roda do mesmo jeito.
Qualquer objeto com __enter__ e __exit__ pode ser usado assim, então você pode colocar os seus próprios recursos atrás da mesma instrução.
O que os dois métodos fazem
__enter__ roda primeiro e o valor que ele devolve é o que o as liga a um nome. __exit__ roda no fim e recebe qual exceção, se houver, está em curso.
# what with actually guarantees
import io
class Tracked:
def __init__(self):
self.events = []
def __enter__(self):
self.events.append('enter')
return self
def __exit__(self, exc_type, exc, tb):
self.events.append(f'exit ({exc_type.__name__ if exc_type else "no error"})')
return False
t = Tracked()
with t as handle:
handle.events.append('body')
print(t.events)
Ele imprime:
['enter', 'body', 'exit (no error)']
A limpeza roda também na saída por erro
A exceção continua o caminho dela depois que o __exit__ rodou.
# cleanup runs even when the body raises
t2 = Tracked()
try:
with t2:
t2.events.append('body')
raise ValueError('boom')
except ValueError as err:
t2.events.append(f'caught {err}')
print(t2.events)
Ele imprime:
['enter', 'body', 'exit (ValueError)', 'caught boom']
É esse o objetivo todo. Um try/finally faria o mesmo, mas o with coloca a limpeza ao lado do recurso, em vez de em cada ponto de chamada.
Retornar True engole a exceção
Um valor verdadeiro retornado pelo __exit__ significa “eu tratei isso”.
# returning True from __exit__ swallows the exception
class Swallow:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return True
with Swallow():
raise RuntimeError('this never escapes')
print('still running')
Ele imprime:
still running
Use isso raramente e de propósito. Engolir toda exceção em silêncio é como erros desaparecem.
O atalho do contextlib
Um generator com um único yield vira um context manager. Tudo antes do yield é preparação, tudo depois é limpeza.
# contextlib turns a generator into a context manager
import contextlib
@contextlib.contextmanager
def tag(name, out):
out.append(f'<{name}>')
try:
yield out
finally:
out.append(f'</{name}>')
lines = []
with tag('p', lines):
lines.append('hello')
print(''.join(lines))
Ele imprime:
<p>hello</p>
O try/finally em volta do yield é o que faz a limpeza rodar quando o corpo levanta uma exceção. Sem ele, a exceção pula o resto do generator.
suppress no lugar de try/except/pass
contextlib.suppress diz o que quer dizer e ocupa uma linha.
# contextlib.suppress instead of try/except/pass
with contextlib.suppress(ZeroDivisionError):
1 / 0
print('suppressed')
Ele imprime:
suppressed
Vários managers de uma vez
Vírgulas aninham eles da esquerda para a direita, e eles fecham na ordem inversa.
# several managers on one line
buf1, buf2 = io.StringIO(), io.StringIO()
with buf1 as a, buf2 as b:
a.write('one')
b.write('two')
print(a.getvalue(), b.getvalue()) # read before the block closes them
print('both closed:', buf1.closed, buf2.closed)
Ele imprime:
one two
both closed: True True
Repare na segunda linha. Assim que o bloco termina, os arquivos estão fechados — então leia o que você precisa lá dentro.
O que lembrar
-
__enter__devolve o que oasliga a um nome,__exit__sempre roda. -
Um retorno verdadeiro do
__exit__engole a exceção. -
@contextlib.contextmanagermaistry/finallyé o jeito curto de escrever um. -
Managers na mesma linha de
withfecham na ordem inversa.
Locks, transações de banco, diretórios temporários, cronômetros e configurações mockadas são todos context managers na biblioteca padrão. Se o seu código tem uma preparação e um encerramento correspondente, ele também quer ser um.