with manager español context python with-statement contextmanager

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 un try...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.