Capturar una excepción al usar una declaración Python 'con'

293

Para mi vergüenza, no puedo entender cómo manejar la excepción para la declaración 'con' python. Si tengo un código:

with open("a.txt") as f:
    print f.readlines()

Realmente quiero manejar 'excepción de archivo no encontrado' para hacer algo. Pero no puedo escribir

with open("a.txt") as f:
    print f.readlines()
except:
    print 'oops'

y no puedo escribir

with open("a.txt") as f:
    print f.readlines()
else:
    print 'oops'

encerrar 'con' en una declaración try / except no funciona más: no se genera una excepción. ¿Qué puedo hacer para procesar la falla dentro de la declaración 'con' de una manera pitónica?

grigoryvp
fuente
¿Qué quiere decir "encerrar 'con' en una declaración try / except no funciona más: no se genera una excepción" ? Una withdeclaración no rompe mágicamente una try...exceptdeclaración circundante .
Aran-Fey
44
Curiosamente, de Java sentencia try-con-recursos hace apoyar exactamente este caso de uso que desee. docs.oracle.com/javase/tutorial/essential/exceptions/…
Nayuki

Respuestas:

256
from __future__ import with_statement

try:
    with open( "a.txt" ) as f :
        print f.readlines()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
    print 'oops'

Si desea un manejo diferente de los errores de la llamada abierta frente al código de trabajo, puede hacer lo siguiente:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()
Douglas Leeder
fuente
3
Como se señaló en stackoverflow.com/questions/5205811/… , el bloque de prueba aquí es realmente demasiado amplio. No se hace distinción entre las excepciones al crear el administrador de contexto y las del cuerpo de la declaración with, por lo que puede no ser una solución válida para todos los casos de uso.
ncoghlan
@ncoghlan Pero puede agregar try...exceptbloques adicionales dentro withpara estar más cerca de la fuente de una excepción que no tiene nada que ver open().
rbaleksandar
1
@rbaleksandar Si recuerdo correctamente, mi comentario se refería estrictamente al primer ejemplo en la respuesta, donde toda la declaración with está dentro del bloque try / except (por lo que, incluso si tiene bloques try / expect internos, cualquier excepción que dejen escapar se Todavía golpeó el exterior). Posteriormente, Douglas agregó el segundo ejemplo para abordar los casos en que esa distinción es importante.
ncoghlan
3
¿Se cerrará el archivo en este ejemplo? Pregunto porque se abrió fuera del alcance "con".
Mike Collins
66
@MikeCollins Salir del 'con' cerrará el archivo abierto incluso cuando el archivo esté abierto antes del 'con'.
user7938784
75

La mejor manera "Pythonic" de hacer esto, explotando la withdeclaración, se enumera como el Ejemplo # 6 en PEP 343 , que proporciona los antecedentes de la declaración.

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

Usado de la siguiente manera:

with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")
jscs
fuente
38
Me gusta pero se siente como un poco de magia negra. No es del todo explícito para el lector
Paul Seeb
55
@PaulSeeb ¿Por qué no lo definirías y te salvarías de hacerlo cada vez que lo necesites? Está definido en el nivel de su aplicación, y es tan mágico como cualquier otro administrador de contexto. Creo que alguien que use la declaración with lo entendería claramente (el nombre de la función también podría ser más expresivo si no le gusta). La declaración "with" en sí misma ha sido diseñada para funcionar de esta manera, para definir un bloque de código "seguro" y delegar funciones de verificación a los administradores de contexto (para que el código sea más claro).
99
Todo este problema solo por no escribir el bloque finalmente en el código de usuario. Estoy empezando a pensar que todos sufrimos por un largo síntoma de exageración en la declaración with.
jgomo3
1
¿La mejor manera de manejar excepciones en python es escribir una función que las capture y las devuelva? ¿Seriamente? La forma pitónica de manejar excepciones es usar una try...exceptdeclaración.
Aran-Fey
58

Capturar una excepción al usar una declaración Python 'con'

La declaración with ha estado disponible sin la __future__importación desde Python 2.6 . Puede obtenerlo tan pronto como Python 2.5 (¡pero en este punto es hora de actualizar!) Con:

from __future__ import with_statement

Aquí está lo más cercano a corregir que tienes. Ya casi estás allí, pero withno tiene una exceptcláusula:

with open("a.txt") as f: 
    print(f.readlines())
except:                    # <- with doesn't have an except clause.
    print('oops')

El __exit__método de un administrador de contexto , si regresa, Falsevolverá a generar el error cuando finalice. Si regresa True, lo suprimirá. El openincorporado __exit__no regresa True, por lo que solo debe anidarlo en un intento, excepto el bloque:

try:
    with open("a.txt") as f:
        print(f.readlines())
except Exception as error: 
    print('oops')

Y repetitivo estándar: no use un simple except:que atrapa BaseExceptiony cualquier otra posible excepción y advertencia. Sea al menos tan específico como Exception, y para este error, tal vez atrape IOError. Solo detecte errores que esté preparado para manejar.

Entonces, en este caso, harías:

>>> try:
...     with open("a.txt") as f:
...         print(f.readlines())
... except IOError as error: 
...     print('oops')
... 
oops
Aaron Hall
fuente
2

Diferenciar entre los posibles orígenes de excepciones planteadas a partir de una withdeclaración compuesta

Diferenciar entre excepciones que ocurren en una withdeclaración es complicado porque pueden originarse en diferentes lugares. Se pueden generar excepciones desde cualquiera de los siguientes lugares (o funciones llamadas allí):

  • ContextManager.__init__
  • ContextManager.__enter__
  • el cuerpo de la with
  • ContextManager.__exit__

Para obtener más detalles, consulte la documentación sobre Tipos de Context Manager .

Si queremos distinguir entre estos diferentes casos, simplemente envolviendo el with en a try .. exceptno es suficiente. Considere el siguiente ejemplo (usando ValueErrorcomo ejemplo pero, por supuesto, podría ser sustituido por cualquier otro tipo de excepción):

try:
    with ContextManager():
        BLOCK
except ValueError as err:
    print(err)

Aquí exceptcapturará excepciones que se originan en los cuatro lugares diferentes y, por lo tanto, no permite distinguir entre ellas. Si movemos la instanciación del objeto del administrador de contexto fuera del with, podemos distinguir entre__init__ y BLOCK / __enter__ / __exit__:

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        with mgr:
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        # At this point we still cannot distinguish between exceptions raised from
        # __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
        pass

Efectivamente, esto solo ayudó con la __init__parte, pero podemos agregar una variable centinela adicional para verificar si el cuerpo delwith inicio comenzó a ejecutarse (es decir, diferenciar entre __enter__y los demás):

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        entered_body = False
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        else:
            # At this point we know the exception came either from BLOCK or from __exit__
            pass

La parte difícil es diferenciar entre las excepciones que se originan BLOCKy __exit__porque se withpasará una excepción que escapa del cuerpo de la voluntad a la __exit__cual puede decidir cómo manejarla (ver los documentos ). Sin embargo __exit__, si surge, la excepción original será reemplazada por la nueva. Para tratar estos casos, podemos agregar una exceptcláusula general en el cuerpo del testamento simplemente no se ejecutará).with archivo para almacenar cualquier posible excepción que de otro modo hubiera pasado desapercibida y compararla con la capturada en el exterior exceptmás adelante; si son iguales, esto significa que el origen fue BLOCKo de lo contrario lo fue __exit__(en caso de que __exit__suprima la excepción al devolver un valor verdadero lo más externoexcept

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    entered_body = exc_escaped_from_body = False
    try:
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
            except Exception as err:  # this exception would normally escape without notice
                # we store this exception to check in the outer `except` clause
                # whether it is the same (otherwise it comes from __exit__)
                exc_escaped_from_body = err
                raise  # re-raise since we didn't intend to handle it, just needed to store it
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        elif err is exc_escaped_from_body:
            print('BLOCK raised:', err)
        else:
            print('__exit__ raised:', err)

Enfoque alternativo utilizando la forma equivalente mencionada en PEP 343

PEP 343 - La declaración "con" especifica una versión equivalente "no con" de la withdeclaración. Aquí podemos envolver fácilmente las diversas partes try ... excepty así diferenciar entre las diferentes fuentes de error potenciales:

import sys

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        value = type(mgr).__enter__(mgr)
    except ValueError as err:
        print('__enter__ raised:', err)
    else:
        exit = type(mgr).__exit__
        exc = True
        try:
            try:
                BLOCK
            except TypeError:
                pass
            except:
                exc = False
                try:
                    exit_val = exit(mgr, *sys.exc_info())
                except ValueError as err:
                    print('__exit__ raised:', err)
                else:
                    if not exit_val:
                        raise
        except ValueError as err:
            print('BLOCK raised:', err)
        finally:
            if exc:
                try:
                    exit(mgr, None, None, None)
                except ValueError as err:
                    print('__exit__ raised:', err)

Por lo general, un enfoque más simple funcionará bien

La necesidad de tal manejo de excepciones especiales debe ser bastante rara y normalmente envolver todo withen un try ... exceptbloque será suficiente. Especialmente si las diversas fuentes de error están indicadas por diferentes tipos de excepción (personalizados) (los gestores de contexto deben diseñarse en consecuencia) podemos distinguir fácilmente entre ellos. Por ejemplo:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...
un invitado
fuente