¿Son los bloques try / except anidados en Python una buena práctica de programación?

201

Estoy escribiendo mi propio contenedor, que necesita dar acceso a un diccionario dentro mediante llamadas de atributo. El uso típico del contenedor sería así:

dict_container = DictContainer()
dict_container['foo'] = bar
...
print dict_container.foo

Sé que podría ser estúpido escribir algo como esto, pero esa es la funcionalidad que necesito proporcionar. Estaba pensando en implementar esto de la siguiente manera:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        try:
            return self.dict[item]
        except KeyError:
            print "The object doesn't have such attribute"

No estoy seguro de si los bloques try / except anidados son una buena práctica, por lo que otra forma sería usar hasattr()y has_key():

def __getattribute__(self, item):
        if hasattr(self, item):
            return object.__getattribute__(item)
        else:
            if self.dict.has_key(item):
                return self.dict[item]
            else:
                raise AttributeError("some customised error")

O para usar uno de ellos y un bloque de captura como este:

def __getattribute__(self, item):
    if hasattr(self, item):
        return object.__getattribute__(item)
    else:
        try:
            return self.dict[item]
        except KeyError:
            raise AttributeError("some customised error")

¿Qué opción es más pitónica y elegante?

Michal
fuente
Agradecería que los dioses pitón proporcionaran if 'foo' in dict_container:. Amén.
gseattle

Respuestas:

180

Tu primer ejemplo está perfectamente bien. Incluso los documentos oficiales de Python recomiendan este estilo conocido como EAFP .

Personalmente, prefiero evitar el anidamiento cuando no es necesario:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass  # fallback to dict
    try:
        return self.dict[item]
    except KeyError:
        raise AttributeError("The object doesn't have such attribute") from None

PD. has_key()ha quedado en desuso durante mucho tiempo en Python 2. Use en su item in self.dictlugar.

lqc
fuente
2
return object.__getattribute__(item)es incorrecto y producirá un TypeErrorporque se pasa el número incorrecto de argumentos. En cambio debería ser return object.__getattribute__(self, item).
martineau
13
PEP 20: plano es mejor que anidado.
Ioannis Filippidis
77
¿Qué significa el from Noneen la última línea?
Niklas
2
@niklas Esencialmente suprime el contexto de excepción ("durante el manejo de esta excepción se produjo otra excepción" -esque mensajes). Ver aquí
Kade
El hecho de que los documentos de Python recomienden los intentos de anidación es una locura. Obviamente es un estilo horrible. La forma correcta de manejar una cadena de operaciones donde las cosas pueden fallar de esa manera sería usar algún tipo de construcción monádica, que Python no admite.
Henry Henrinson
19

Si bien en Java es una mala práctica usar Excepciones para el control de flujo (principalmente porque las excepciones obligan a jvm a reunir recursos ( más aquí )), en Python tiene 2 principios importantes: Duck Typing y EAFP . Básicamente, esto significa que se le recomienda que intente usar un objeto de la forma en que cree que funcionaría, y manejarlo cuando las cosas no sean así.

En resumen, el único problema sería que su código se sangrara demasiado. Si lo desea, intente simplificar algunos de los anidamientos como lqc sugirió

Bruno Penteado
fuente
10

Para su ejemplo específico, en realidad no necesita anidarlos. Si la expresión en el trybloque tiene éxito, la función volverá, por lo que cualquier código después del bloque try / except completo solo se ejecutará si falla el primer intento. Entonces solo puedes hacer:

def __getattribute__(self, item):
    try:
        return object.__getattribute__(item)
    except AttributeError:
        pass
    # execution only reaches here when try block raised AttributeError
    try:
        return self.dict[item]
    except KeyError:
        print "The object doesn't have such attribute"

Anidarlos no es malo, pero siento que dejarlo plano hace que la estructura sea más clara: estás probando secuencialmente una serie de cosas y devolviendo la primera que funciona.

Por cierto, es posible que desee pensar si realmente desea usar en __getattribute__lugar de __getattr__aquí. El uso __getattr__simplificará las cosas porque sabrá que el proceso normal de búsqueda de atributos ya ha fallado.

BrenBarn
fuente
10

Solo tenga cuidado, en este caso primero finallyse toca PERO también se omite.

def a(z):
    try:
        100/z
    except ZeroDivisionError:
        try:
            print('x')
        finally:
            return 42
    finally:
        return 1


In [1]: a(0)
x
Out[1]: 1
Sławomir Lenart
fuente
Wow, me sorprende ... ¿Podría señalarme algún fragmento de documentación que explique este comportamiento?
Michal
1
@Michal: fyi: ambos finallybloques se ejecutan para a(0), pero solo finally-returnse devuelve el padre .
Sławomir Lenart
7

En mi opinión, esta sería la forma más pitónica de manejarlo, aunque y porque hace que su pregunta sea discutible. Tenga en cuenta que esto define en __getattr__()lugar de __getattribute__()porque hacerlo significa que solo tiene que tratar con los atributos "especiales" que se mantienen en el diccionario interno.

def __getattr__(self, name):
    """only called when an attribute lookup in the usual places has failed"""
    try:
        return self.my_dict[name]
    except KeyError:
        raise AttributeError("some customized error message")
Martineau
fuente
2
Tenga en cuenta que generar una excepción en un exceptbloque puede generar resultados confusos en Python 3. Esto se debe a que (según PEP 3134) Python 3 rastrea la primera excepción (the KeyError) como el "contexto" de la segunda excepción (the AttributeError), y si alcanza el nivel superior, imprimirá un rastreo que incluye ambas excepciones. Esto puede ser útil cuando no se esperaba una segunda excepción, pero si está provocando deliberadamente la segunda excepción, no es deseable. Para Python 3.3, PEP 415 agregó la capacidad de suprimir el contexto mediante el uso raise AttributeError("whatever") from None.
Blckknght
3
@Blckknght: Imprimir un rastreo que incluya ambas excepciones estaría bien en este caso. En otras palabras, no creo que su declaración general de que siempre sea indeseable sea cierta. En el uso aquí, se está convirtiendo de una KeyErroren una AttributeErrory mostrando que es lo que ocurrió en un rastreo serían útiles y apropiadas.
Martineau
Para situaciones más complicadas puede que tenga razón, pero creo que cuando realiza la conversión entre tipos de excepción, a menudo sabe que los detalles de la primera excepción no son importantes para el usuario externo. Es decir, si se produce __getattr__una excepción, el error probablemente sea un error tipográfico en el acceso al atributo, no un error de implementación en el código de la clase actual. Mostrar la excepción anterior como contexto puede confundir eso. E incluso cuando suprime el contexto con raise Whatever from None, aún puede obtener la excepción anterior si es necesario a través de ex.__context__.
Blckknght
1
Quería aceptar su respuesta, sin embargo, en la pregunta tenía más curiosidad sobre si usar o no el bloque anidado try / catch es una buena práctica. Por otro lado, es la solución más elegante y la voy a usar en mi código. Muchas gracias Martin.
Michal
Michal: De nada. También es más rápido que usar __getattribute__().
Martineau
4

En Python es más fácil pedir perdón que permiso. No se preocupe por el manejo de excepciones anidado.

(Además, has*casi siempre usa excepciones bajo la cubierta de todos modos).

Ignacio Vazquez-Abrams
fuente
4

Según la documentación , es mejor manejar múltiples excepciones a través de tuplas o de esta manera:

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except IOError as e:
    print "I/O error({0}): {1}".format(e.errno, e.strerror)
except ValueError:
    print "Could not convert data to an integer."
except:
    print "Unexpected error:", sys.exc_info()[0]
    raise
Blairg23
fuente
2
Esta respuesta realmente no aborda la pregunta original, pero para cualquiera que la lea, tenga en cuenta que "al descubierto", excepto que al final es una idea terrible (generalmente), ya que atrapará todo, incluyendo, por ejemplo, NameError & KeyboardInterrupt, ¡lo cual no es lo que usted quiso decir!
perdido el
Dado que el código vuelve a generar la misma excepción justo después de la declaración impresa, ¿es realmente un gran problema? En este caso, puede proporcionar más contexto sobre la excepción sin ocultarlo. Si no volviera a subir, estaría totalmente de acuerdo, pero no creo que exista el riesgo de ocultar una excepción que no tenía la intención.
NimbusScale
4

Un buen y simple ejemplo para try / except anidado podría ser el siguiente:

import numpy as np

def divide(x, y):
    try:
        out = x/y
    except:
        try:
            out = np.inf * x / abs(x)
        except:
            out = np.nan
    finally:
        return out

Ahora prueba varias combinaciones y obtendrás el resultado correcto:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[por supuesto, tenemos numpy, así que no necesitamos crear esta función]

Cuidado devastador
fuente
2

Una cosa que me gusta evitar es plantear una nueva excepción al manejar una antigua. Hace que los mensajes de error sean confusos de leer.

Por ejemplo, en mi código, originalmente escribí

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    raise KeyError(key)

Y recibí este mensaje.

>>> During handling of above exception, another exception occurred.

Lo que quería era esto:

try:
    return tuple.__getitem__(self, i)(key)
except IndexError:
    pass
raise KeyError(key)

No afecta cómo se manejan las excepciones. En cualquier bloque de código, se habría detectado un KeyError. Esto es simplemente una cuestión de obtener puntos de estilo.

Steve Zelaznik
fuente
Sin from Noneembargo, vea el uso de aumento de la respuesta aceptada para obtener aún más puntos de estilo. :)
Pianosaurus
1

Si try-except-finally está anidado dentro del bloque finalmente, el resultado de "hijo" finalmente se conserva. Todavía no he encontrado la expansión oficial, pero el siguiente fragmento de código muestra este comportamiento en Python 3.6.

def f2():
    try:
        a = 4
        raise SyntaxError
    except SyntaxError as se:
        print('log SE')
        raise se from None
    finally:
        try:
            raise ValueError
        except ValueError as ve:
            a = 5
            print('log VE')
            raise ve from None
        finally:
            return 6       
        return a

In [1]: f2()
log SE
log VE
Out[2]: 6
Guanghua Shu
fuente
Este comportamiento es diferente del ejemplo dado por @Sławomir Lenart cuando finalmente está anidado dentro, excepto el bloque.
Guanghua Shu
0

No creo que sea cuestión de ser pitónico o elegante. Se trata de evitar excepciones tanto como sea posible. Las excepciones están destinadas a manejar errores que pueden ocurrir en código o eventos sobre los que no tiene control. En este caso, tiene control total al verificar si un elemento es un atributo o en un diccionario, por lo tanto, evite las excepciones anidadas y continúe con su segundo intento.

owobeid
fuente
De los documentos: en un entorno de subprocesos múltiples, el enfoque LBYL (Look Before You Leap) puede arriesgarse a introducir una condición de carrera entre "mirar" y "saltar". Por ejemplo, el código, si la clave en la asignación: la asignación de retorno [clave] puede fallar si otro hilo elimina la clave de la asignación después de la prueba, pero antes de la búsqueda. Este problema se puede resolver con bloqueos o utilizando el enfoque
Nuno André