manager - ¿Es Python*con*instrucción exactamente equivalente a un intento-(excepto)-finalmente bloquear?
with python español (1)
Sé que esto fue ampliamente discutido, pero todavía no puedo encontrar una respuesta para confirmar esto: es la sentencia with idéntica a llamar al mismo código en un bloque try - (excepto) -finally, donde cualquiera que se defina en la función __exit__
de El gestor de contexto se coloca en el bloque finalmente?
Por ejemplo, ¿estos 2 fragmentos de código hacen exactamente lo mismo?
import sys
from contextlib import contextmanager
@contextmanager
def open_input(fpath):
fd = open(fpath) if fpath else sys.stdin
try:
yield fd
finally:
fd.close()
with open_input("/path/to/file"):
print "starting to read from file..."
lo mismo que:
def open_input(fpath):
try:
fd = open(fpath) if fpath else sys.stdin
print "starting to read from file..."
finally:
fd.close()
open_input("/path/to/file")
¡Gracias!
Voy a dejar de lado las menciones de alcance, porque en realidad no es muy relevante.
Según PEP 343 ,
with EXPR as VAR:
BLOCK
traduce a
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
Como puede ver, type(mgr).__enter__
se llama como espera, pero no dentro del try
.
type(mgr).__exit__
se llama en exit. La única diferencia es que cuando hay una excepción, se toma la ruta if not exit(mgr, *sys.exc_info())
. Esto permite la introspección y el silencio de los errores, a diferencia de lo que puede hacer una cláusula finally
.
contextmanager
no complica mucho esto. Es solo
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, *args, **kwds)
return helper
Luego mira la clase en cuestión:
class _GeneratorContextManager(ContextDecorator):
def __init__(self, func, *args, **kwds):
self.gen = func(*args, **kwds)
def __enter__(self):
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn''t yield") from None
def __exit__(self, type, value, traceback):
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn''t stop")
else:
if value is None:
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn''t stop after throw()")
except StopIteration as exc:
return exc is not value
except:
if sys.exc_info()[1] is not value:
raise
Código no importante ha sido elidido.
Lo primero que se debe tener en cuenta es que si hay varios yield
, este código generará un error.
Esto no afecta notablemente al flujo de control.
Considere __enter__
.
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn''t yield") from None
Si el administrador de contexto estaba bien escrito, esto nunca se romperá de lo que se espera.
Una diferencia es que si el generador lanza StopIteration
, se StopIteration
un error diferente ( RuntimeError
). Esto significa que el comportamiento no es totalmente idéntico al normal si está ejecutando un código completamente arbitrario.
Considere un __exit__ no __exit__
:
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn''t stop")
La única diferencia es como antes; Si su código lanza StopIteration
, afectará al generador y, por lo tanto, el decorador de contextmanager
lo malinterpretará.
Esto significa que:
from contextlib import contextmanager
@contextmanager
def with_cleanup(func):
try:
yield
finally:
func()
def good_cleanup():
print("cleaning")
with with_cleanup(good_cleanup):
print("doing")
1/0
#>>> doing
#>>> cleaning
#>>> Traceback (most recent call last):
#>>> File "", line 15, in <module>
#>>> ZeroDivisionError: division by zero
def bad_cleanup():
print("cleaning")
raise StopIteration
with with_cleanup(bad_cleanup):
print("doing")
1/0
#>>> doing
#>>> cleaning
Lo que es poco probable que importe, pero podría.
Finalmente:
else:
if value is None:
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn''t stop after throw()")
except StopIteration as exc:
return exc is not value
except:
if sys.exc_info()[1] is not value:
raise
Esto plantea la misma pregunta sobre StopIteration
, pero es interesante notar esa última parte.
if sys.exc_info()[1] is not value:
raise
Esto significa que si la excepción no se maneja, el rastreo no se modificará. Si se manejó pero existe un nuevo rastreo, se levantará en su lugar.
Esto coincide perfectamente con la especificación.
TL; DR
with
es en realidad un poco más poderoso que untry...finally
en el que el introspección puede y los errores de silencio.Tenga cuidado con
StopIteration
, pero de lo contrario está bien usando@contextmanager
para crear administradores de contexto.