Les blocs try / except imbriqués en python sont-ils une bonne pratique de programmation?

201

J'écris mon propre conteneur, qui doit donner accès à un dictionnaire à l'intérieur par des appels d'attribut. L'utilisation typique du conteneur serait comme ceci:

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

Je sais qu'il peut être stupide d'écrire quelque chose comme ça, mais c'est la fonctionnalité que je dois fournir. Je pensais à mettre en œuvre cela de la manière suivante:

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"

Je ne sais pas si les blocs try / except imbriqués sont une bonne pratique, donc une autre façon serait d'utiliser hasattr()et 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")

Ou pour en utiliser un et essayer un bloc catch comme ceci:

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")

Quelle option est la plus pythonique et élégante?

Michal
la source
J'apprécierais les dieux de python fournissant if 'foo' in dict_container:. Amen.
gseattle

Réponses:

180

Votre premier exemple est parfaitement bien. Même les documents officiels de Python recommandent ce style appelé EAFP .

Personnellement, je préfère éviter d'imbriquer quand ce n'est pas nécessaire:

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

PS. has_key()est obsolète depuis longtemps dans Python 2. Utilisez-le à la item in self.dictplace.

lqc
la source
2
return object.__getattribute__(item)est incorrect et produira un TypeErrorcar un nombre incorrect d'arguments est transmis. Cela devrait plutôt l'être return object.__getattribute__(self, item).
martineau
13
PEP 20: plat est meilleur que niché.
Ioannis Filippidis
7
Que from Nonesignifie la dernière ligne?
niklas
2
@niklas Il supprime essentiellement le contexte d'exception ("lors de la gestion de cette exception, une autre exception s'est produite" -des messages similaires). Voir ici
Kade
Le fait que les documents Python recommandent des essais d'imbrication est un peu fou. C'est évidemment un style horrible. La façon correcte de gérer une chaîne d'opérations où les choses peuvent échouer comme ça serait d'utiliser une sorte de construction monadique, que Python ne prend pas en charge.
Henry Henrinson
19

Alors qu'en Java c'est en effet une mauvaise pratique d'utiliser les exceptions pour le contrôle de flux (principalement parce que les exceptions forcent le jvm à rassembler des ressources ( plus ici )), en Python vous avez 2 principes importants: Duck Typing et EAFP . Cela signifie essentiellement que vous êtes encouragé à essayer d'utiliser un objet de la façon dont vous pensez qu'il fonctionnerait, et à gérer lorsque les choses ne sont pas comme ça.

En résumé, le seul problème serait que votre code soit trop indenté. Si vous en avez envie, essayez de simplifier certains des emboîtements comme suggéré par lqc

Bruno Penteado
la source
10

Pour votre exemple spécifique, vous n'avez pas réellement besoin de les imbriquer. Si l'expression dans le trybloc réussit, la fonction retournera, donc tout code après le bloc try / except entier ne sera exécuté que si la première tentative échoue. Vous pouvez donc simplement faire:

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"

Les imbriquer n'est pas mauvais, mais j'ai l'impression que le laisser à plat rend la structure plus claire: vous essayez séquentiellement une série de choses et retournez la première qui fonctionne.

Soit dit en passant, vous voudrez peut-être vous demander si vous voulez vraiment utiliser __getattribute__plutôt __getattr__qu'ici. L'utilisation __getattr__simplifiera les choses, car vous saurez que le processus de recherche d'attribut normal a déjà échoué.

BrenBarn
la source
10

Faites juste attention - dans ce cas, finallyon touche d'abord MAIS sauté aussi.

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
la source
Wow ça me souffle l'esprit ... Pourriez-vous me montrer un fragment de documentation expliquant ce comportement?
Michal
1
@Michal: fyi: les deux finallyblocs sont exécutés pour a(0), mais seul le parent finally-returnest retourné.
Sławomir Lenart
7

À mon avis, ce serait la façon la plus pythonique de le gérer, bien que cela rende votre question sans objet. Notez que cela définit __getattr__()au lieu de __getattribute__()parce que cela signifie qu'il n'a à gérer que les attributs "spéciaux" conservés dans le dictionnaire interne.

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
la source
2
Notez que lever une exception dans un exceptbloc peut donner une sortie déroutante dans Python 3. C'est parce que (selon PEP 3134) Python 3 suit la première exception (la KeyError) comme le "contexte" de la deuxième exception (la AttributeError), et si elle atteint le de niveau supérieur, il imprimera une trace qui inclut les deux exceptions. Cela peut être utile lorsqu'une deuxième exception n'était pas attendue, mais si vous soulevez délibérément la deuxième exception, ce n'est pas souhaitable. Pour Python 3.3, PEP 415 a ajouté la possibilité de supprimer le contexte en utilisant raise AttributeError("whatever") from None.
Blckknght
3
@Blckknght: L'impression d'un traçage qui inclut les deux exceptions serait bien dans ce cas. En d'autres termes, je ne pense pas que votre déclaration générale selon laquelle c'est toujours indésirable soit vraie. Dans l'utilisation ici, cela transforme un KeyErroren AttributeErroret montre que ce qui s'est passé dans un retraçage serait utile et approprié.
martineau
Pour des situations plus compliquées, vous avez peut-être raison, mais je pense que lorsque vous convertissez entre des types d'exception, vous savez souvent que les détails de la première exception n'ont pas d'importance pour l'utilisateur externe. Autrement dit, si __getattr__déclenche une exception, le bogue est probablement une faute de frappe dans l'accès aux attributs, pas un bogue d'implémentation dans le code de la classe actuelle. Montrer l'exception précédente comme contexte peut brouiller cela. Et même lorsque vous supprimez le contexte avec raise Whatever from None, vous pouvez toujours obtenir l'exception précédente si nécessaire via ex.__context__.
Blckknght
1
Je voulais accepter votre réponse, mais dans la question, j'étais plus curieux de savoir si l'utilisation du bloc try / catch imbriqué est une bonne pratique. D'un autre côté, c'est la solution la plus élégante et je vais l'utiliser dans mon code. Merci beaucoup Martin.
Michal
Michal: Je vous en prie. C'est aussi plus rapide que d'utiliser __getattribute__().
martineau
4

En Python, il est plus facile de demander pardon que permission. Ne transpirez pas la gestion des exceptions imbriquées.

(De plus, has*il utilise presque toujours des exceptions sous le couvert de toute façon.)

Ignacio Vazquez-Abrams
la source
4

Selon la documentation , il est préférable de gérer plusieurs exceptions via des tuples ou comme ceci:

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
la source
2
Cette réponse ne répond pas vraiment à la question d'origine, mais pour tous ceux qui la lisent, notez le "nu" sauf à la fin est une idée terrible (généralement) car elle va tout attraper, y compris par exemple NameError & KeyboardInterrupt - ce qui n'est généralement pas ce que vous vouliez dire!
perdu le
Étant donné que le code relance la même exception juste après l'instruction print, est-ce vraiment un gros problème. Dans ce cas, il peut fournir plus de contexte sur l'exception sans la masquer. Si elle ne sur-relance pas, je serais tout à fait d'accord, mais je ne pense pas qu'il y ait un risque à cacher une exception que vous n'aviez pas l'intention de faire.
NimbusScale
4

Un bon et simple exemple d'imbrication try / except pourrait être le suivant:

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

Essayez maintenant différentes combinaisons et vous obtiendrez le résultat correct:

divide(15, 3)
# 5.0

divide(15, 0)
# inf

divide(-15, 0)
# -inf

divide(0, 0)
# nan

[bien sûr, nous avons numpy donc nous n'avons pas besoin de créer cette fonction]

Soins ravageurs
la source
2

Une chose que j'aime éviter est de lever une nouvelle exception lors de la gestion d'une ancienne. Cela rend les messages d'erreur déroutants à lire.

Par exemple, dans mon code, j'ai écrit à l'origine

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

Et j'ai reçu ce message.

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

Ce que je voulais c'était:

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

Cela n'affecte pas la façon dont les exceptions sont gérées. Dans l'un ou l'autre bloc de code, une erreur KeyError aurait été interceptée. Il s'agit simplement d'obtenir des points de style.

Steve Zelaznik
la source
Voir l'utilisation de la réponse acceptée pour augmenter from None, pour encore plus de points de style. :)
Pianosaurus
1

Si try-except-finally est imbriqué dans le bloc finally, le résultat de "l'enfant" est finalement conservé. Je n'ai pas encore trouvé d'expiration officielle, mais l'extrait de code suivant montre ce comportement dans 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
la source
Ce comportement est différent de l'exemple donné par @ Sławomir Lenart lorsqu'il est finalement imbriqué à l'intérieur sauf block.
Guanghua Shu
0

Je ne pense pas qu'il s'agisse d'être pythonique ou élégant. Il s'agit d'empêcher autant que possible les exceptions. Les exceptions sont destinées à gérer les erreurs pouvant survenir dans le code ou les événements sur lesquels vous n'avez aucun contrôle. Dans ce cas, vous avez un contrôle total lorsque vous vérifiez si un élément est un attribut ou dans un dictionnaire, évitez donc les exceptions imbriquées et respectez votre deuxième tentative.

Owobeid
la source
De la documentation: Dans un environnement multi-thread, l'approche LBYL (Look Before You Leap) peut risquer d'introduire une condition de concurrence entre «le regard» et «le saut». Par exemple, le code, si clé dans le mappage: le mappage de retour [clé] peut échouer si un autre thread supprime la clé du mappage après le test, mais avant la recherche. Ce problème peut être résolu avec des verrous ou en utilisant l'approche EAFP (plus facile à demander pardon qu'à l'autorisation) .
Nuno André