with existe para que la limpieza ocurra pase lo que pase en el cuerpo. Salgas antes, lances una excepción o termines normal, el código de salida corre igual.
Cualquier objeto con __enter__ y __exit__ se puede usar así, o sea que puedes poner tus propios recursos detrás de la misma instrucción.
Qué hacen los dos métodos
__enter__ corre primero y lo que devuelve es lo que se asigna con as. __exit__ corre al final y le dicen qué excepción va en curso, si es que hay alguna.
# 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)
Imprime:
['enter', 'body', 'exit (no error)']
La limpieza corre también al salir por un error
La excepción sigue su camino después de que __exit__ corrió.
# 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)
Imprime:
['enter', 'body', 'exit (ValueError)', 'caught boom']
Ese es todo el punto. Un try/finally haría lo mismo, pero with pone la limpieza junto al recurso en lugar de en cada lugar donde se usa.
Devolver True se traga la excepción
Un valor verdadero devuelto por __exit__ significa «yo me encargué».
# 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')
Imprime:
still running
Usa esto pocas veces y a propósito. Tragarse todas las excepciones en silencio es la forma en que los errores desaparecen.
El atajo de contextlib
Un generador con un solo yield se vuelve un context manager. Todo lo que va antes del yield es preparación, todo lo que va después es limpieza.
# 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))
Imprime:
<p>hello</p>
El try/finally alrededor del yield es lo que hace que la limpieza corra cuando el cuerpo lanza una excepción. Sin él, una excepción se salta el resto del generador.
suppress en lugar de try/except/pass
contextlib.suppress dice lo que hace y ocupa una línea.
# contextlib.suppress instead of try/except/pass
with contextlib.suppress(ZeroDivisionError):
1 / 0
print('suppressed')
Imprime:
suppressed
Varios managers a la vez
Las comas los anidan de izquierda a derecha, y se cierran en orden inverso.
# 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)
Imprime:
one two
both closed: True True
Fíjate en la segunda línea. Cuando el bloque termina los archivos ya están cerrados, así que lee lo que necesites adentro.
Qué recordar
-
__enter__devuelve lo que se asigna conas, y__exit__corre siempre. -
Un valor verdadero devuelto por
__exit__se traga la excepción. -
@contextlib.contextmanagermástry/finallyes la forma corta de escribir uno. -
Los managers puestos en una misma línea de
withse cierran en orden inverso.
Los locks, las transacciones de base de datos, los directorios temporales, los cronómetros y la configuración simulada son todos context managers de la biblioteca estándar. Si tu código tiene una preparación y una limpieza que le corresponde, también quiere ser uno.