Une bonne façon de déclarer des exceptions personnalisées dans Python moderne?

1292

Quelle est la bonne façon de déclarer des classes d'exceptions personnalisées en Python moderne? Mon objectif principal est de suivre ce que les autres classes d'exceptions standard ont, de sorte que (par exemple) toute chaîne supplémentaire que j'inclus dans l'exception soit imprimée par l'outil qui a détecté l'exception.

Par «Python moderne», je veux dire quelque chose qui fonctionnera en Python 2.5 mais qui sera «correct» pour la façon de faire Python 2.6 et Python 3. *. Et par "personnalisé", j'entends un objet Exception qui peut inclure des données supplémentaires sur la cause de l'erreur: une chaîne, peut-être aussi un autre objet arbitraire pertinent pour l'exception.

J'ai été déclenché par l'avertissement de dépréciation suivant dans Python 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

Cela semble fou qui BaseExceptiona une signification particulière pour les attributs nommés message. J'en déduis du PEP-352 que cet attribut avait une signification spéciale dans 2.5 qu'ils essaient de déprécier, donc je suppose que ce nom (et celui-là seul) est maintenant interdit? Pouah.

Je suis également vaguement conscient de l'existence Exceptiond'un paramètre magique args, mais je n'ai jamais su comment l'utiliser. Je ne suis pas sûr non plus que ce soit la bonne façon de faire les choses à l'avenir; une grande partie de la discussion que j'ai trouvée en ligne a suggéré qu'ils essayaient de supprimer les arguments en Python 3.

Mise à jour: deux réponses ont suggéré de remplacer __init__, et __str__/ __unicode__/ __repr__. Cela semble beaucoup taper, est-ce nécessaire?

Nelson
la source

Réponses:

1323

J'ai peut-être manqué la question, mais pourquoi pas:

class MyException(Exception):
    pass

Modifier: pour remplacer quelque chose (ou passer des arguments supplémentaires), procédez comme suit:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

De cette façon, vous pouvez transmettre des messages d'erreur au deuxième paramètre et y accéder plus tard avec e.errors


Mise à jour Python 3: dans Python 3+, vous pouvez utiliser cette utilisation légèrement plus compacte de super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors
gahooa
la source
35
Cependant, une exception définie comme celle-ci ne serait pas sélectionnable; voir la discussion ici stackoverflow.com/questions/16244923/…
jiakai
87
@jiakai signifie "picklable". :-)
Robino
1
Suite à la documentation de python pour les exceptions définies par l'utilisateur, les noms mentionnés dans la fonction __init__ sont incorrects. Au lieu de (auto, message, erreur), c'est (auto, expression, message). L'expression d'attribut est l'expression d'entrée dans laquelle l'erreur s'est produite et le message est une explication de l'erreur.
ddleon
2
C'est un malentendu, @ddleon. L'exemple dans les documents auxquels vous faites référence concerne un cas d'utilisation particulier. Il n'y a aucune signification pour le nom des arguments de constructeur de la sous-classe (ni leur nombre).
asthasr
498

Avec des exceptions Python modernes, vous n'avez pas besoin d'abus .message, ou override .__str__()ou .__repr__()ou l' un de. Si tout ce que vous voulez est un message informatif lorsque votre exception est levée, procédez comme suit:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

Cela donnera un traceback se terminant par MyException: My hovercraft is full of eels.

Si vous voulez plus de flexibilité de l'exception, vous pouvez passer un dictionnaire comme argument:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

Cependant, obtenir ces détails dans un exceptbloc est un peu plus compliqué. Les détails sont stockés dans l' argsattribut, qui est une liste. Vous auriez besoin de faire quelque chose comme ceci:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

Il est toujours possible de passer plusieurs éléments à l'exception et d'y accéder via des index de tuple, mais cela est fortement déconseillé (et était même destiné à la dépréciation il y a quelque temps). Si vous avez besoin de plus d'une seule information et que la méthode ci-dessus ne vous suffit pas, vous devez sous-classer Exceptioncomme décrit dans le didacticiel .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
frnknstn
la source
2
"mais cela sera déprécié à l'avenir" - est-ce toujours destiné à la dépréciation? Python 3.7 semble toujours accepter avec joie Exception(foo, bar, qux).
mtraceur
Il n'a vu aucun travail récent pour le déprécier depuis la dernière tentative a échoué en raison de la douleur de la transition, mais cet usage est toujours déconseillé. Je mettrai à jour ma réponse pour refléter cela.
frnknstn
@frnknstn, pourquoi est-il découragé? On dirait un joli idiome pour moi.
neves
2
@neves pour commencer, utiliser des tuples pour stocker des informations sur les exceptions n'a aucun avantage sur l'utilisation d'un dictionnaire pour faire de même. Si vous êtes intéressé par le raisonnement derrière les changements d'exception, jetez un œil à PEP352
frnknstn
La section pertinente du PEP352 est "Idées rétractées" .
liberforce
196

"Une bonne façon de déclarer des exceptions personnalisées en Python moderne?"

C'est très bien, sauf si votre exception est vraiment un type d'exception plus spécifique:

class MyException(Exception):
    pass

Ou mieux (peut-être parfait), au lieu de passdonner une docstring:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Sous-classes d'exception

À partir des documents

Exception

Toutes les exceptions intégrées qui ne sortent pas du système sont dérivées de cette classe. Toutes les exceptions définies par l'utilisateur doivent également être dérivées de cette classe.

Cela signifie que si votre exception est un type d'exception plus spécifique, sous-classe cette exception au lieu du générique Exception(et le résultat sera que vous dérivez toujours Exceptioncomme le recommandent les documents). De plus, vous pouvez au moins fournir une docstring (et ne pas être obligé d'utiliser le passmot - clé):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Définissez les attributs que vous créez vous-même avec une personnalisation __init__. Évitez de passer un dict comme argument positionnel, les futurs utilisateurs de votre code vous en remercieront. Si vous utilisez l'attribut de message obsolète, son attribution vous-même évitera DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

Il n'y a vraiment pas besoin d'écrire votre propre __str__ou __repr__. Ceux intégrés sont très agréables, et votre héritage coopératif garantit que vous l'utilisez.

Critique de la meilleure réponse

J'ai peut-être manqué la question, mais pourquoi pas:

class MyException(Exception):
    pass

Encore une fois, le problème avec ce qui précède est que pour l'attraper, vous devrez soit le nommer spécifiquement (l'importer s'il est créé ailleurs), soit intercepter l'exception, (mais vous n'êtes probablement pas prêt à gérer tous les types d'exceptions, et vous ne devez intercepter que les exceptions que vous êtes prêt à gérer). Critique similaire à ce qui suit, mais en plus, ce n'est pas la façon d'initialiser via super, et vous obtiendrez un DeprecationWarningsi vous accédez à l'attribut de message:

Modifier: pour remplacer quelque chose (ou passer des arguments supplémentaires), procédez comme suit:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

De cette façon, vous pouvez transmettre des messages d'erreur au deuxième paramètre et y accéder plus tard avec e.errors

Il faut également que deux arguments soient transmis (à part le self.) Pas plus, pas moins. C'est une contrainte intéressante que les futurs utilisateurs pourraient ne pas apprécier.

Pour être direct - cela viole la substituabilité de Liskov .

Je vais démontrer les deux erreurs:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

Par rapport à:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Aaron Hall
la source
2
Bonjour de 2018! BaseException.messageest parti en Python 3, donc la critique ne vaut que pour les anciennes versions, non?
Kos
5
@Kos La critique de la substituabilité de Liskov est toujours valable. La sémantique du premier argument en tant que «message» est également discutable, mais je ne pense pas que je vais contester ce point. Je donnerai plus de détails lorsque j'aurai plus de temps libre.
Aaron Hall
1
FWIW, pour Python 3 (au moins pour 3.6+), on redéfinirait la __str__méthode MyAppValueErrorau lieu de s'appuyer sur l' messageattribut
Jacquot
1
@AaronHall Pourriez-vous développer les avantages de sous-classer ValueError plutôt que Exception? Vous déclarez que c'est ce que signifient les documents, mais une lecture directe ne prend pas en charge cette interprétation et dans le didacticiel Python sous Exceptions définies par l'utilisateur, il en fait clairement le choix des utilisateurs: "Les exceptions doivent généralement être dérivées de la classe Exception, directement ou indirectement. " Désireux donc de comprendre si votre point de vue est justifiable, s'il vous plaît.
ostergaard
1
@ostergaard Je ne peux pas répondre complètement en ce moment, mais en bref, l'utilisateur a la possibilité supplémentaire d'attraper ValueError. Cela a du sens si c'est dans la catégorie des erreurs de valeur. Si ce n'est pas dans la catégorie des erreurs de valeur, je m'y opposerais sur la sémantique. Il y a de la place pour certaines nuances et raisonnements de la part du programmeur, mais je préfère de loin la spécificité, le cas échéant. Je mettrai à jour ma réponse pour mieux aborder le sujet très prochainement.
Aaron Hall
50

voir comment les exceptions fonctionnent par défaut si un ou plusieurs attributs sont utilisés (tracebacks omis):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

donc vous voudrez peut-être avoir une sorte de " modèle d'exception ", fonctionnant comme une exception elle-même, d'une manière compatible:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

cela peut être fait facilement avec cette sous-classe

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

et si vous n'aimez pas cette représentation par défaut comme un tuple, ajoutez simplement une __str__méthode à la ExceptionTemplateclasse, comme:

    # ...
    def __str__(self):
        return ': '.join(self.args)

et vous aurez

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
mykhal
la source
32

Depuis Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), la méthode recommandée est toujours:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

N'oubliez pas de documenter, pourquoi une exception personnalisée est nécessaire!

Si vous en avez besoin, voici la voie à suivre pour les exceptions avec plus de données:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

et les récupérer comme:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=Noneest important pour le rendre décapant. Avant de le vider, vous devez appeler error.__reduce__(). Le chargement fonctionnera comme prévu.

Vous devriez peut-être chercher à trouver une solution à l'aide de l' returninstruction pythons si vous avez besoin de beaucoup de données à transférer vers une structure externe. Cela me semble plus clair / plus pythonique. Les exceptions avancées sont largement utilisées en Java, ce qui peut parfois être ennuyeux, lorsque vous utilisez un framework et que vous devez intercepter toutes les erreurs possibles.

fameman
la source
1
À tout le moins, les documents actuels indiquent que c'est la façon de le faire (au moins sans le __str__) plutôt que d'autres réponses qui utilisent super().__init__(...).. Juste une honte qui remplace __str__et __repr__est probablement nécessaire juste pour une meilleure sérialisation "par défaut".
kevlarr
2
Question honnête: pourquoi est-il important que les exceptions soient décapables? Quels sont les cas d'utilisation des exceptions de dumping et de chargement?
Roel Schroeven
1
@RoelSchroeven: J'ai dû paralléliser le code une fois. Ran fine single process, mais certains aspects de certaines de ses classes n'étaient pas sérialisables (la fonction lambda étant passée en tant qu'objets). Ça m'a pris un peu de temps pour le comprendre et le réparer. Cela signifie que plus tard, quelqu'un pourrait avoir besoin que votre code soit sérialisé, être incapable de le faire et devoir creuser pourquoi ... Mon problème n'était pas des erreurs non récupérables, mais je peux le voir causer des problèmes similaires.
logicOnAbstractions
17

Vous devez remplacer __repr__ou __unicode__méthodes au lieu d'utiliser message, les arguments que vous fournissez lorsque vous construisez l'exception seront dans l' argsattribut de l'objet d'exception.

M. Utku ALTINKAYA
la source
7

Non, le "message" n'est pas interdit. C'est juste obsolète. Votre application fonctionnera bien avec l'utilisation du message. Mais vous voudrez peut-être vous débarrasser de l'erreur de dépréciation, bien sûr.

Lorsque vous créez des classes d'exception personnalisées pour votre application, beaucoup d'entre elles ne sous-classent pas uniquement à partir d'Exception, mais à partir d'autres, comme ValueError ou similaire. Ensuite, vous devez vous adapter à leur utilisation des variables.

Et si vous avez de nombreuses exceptions dans votre application, c'est généralement une bonne idée d'avoir une classe de base personnalisée commune pour toutes, afin que les utilisateurs de vos modules puissent faire

try:
    ...
except NelsonsExceptions:
    ...

Et dans ce cas, vous pouvez faire le __init__ and __str__nécessaire là-bas, vous n'avez donc pas à le répéter pour chaque exception. Mais simplement appeler la variable message autre chose que message fait l'affaire.

Dans tous les cas, vous n'avez besoin que __init__ or __str__si vous faites quelque chose de différent de ce que fait l'Exception elle-même. Et parce que si la dépréciation, vous avez alors besoin des deux, ou vous obtenez une erreur. Ce n'est pas beaucoup de code supplémentaire dont vous avez besoin par classe. ;)

Lennart Regebro
la source
Il est intéressant de noter que les exceptions Django n'héritent pas d'une base commune. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions Avez-vous un bon exemple pour intercepter toutes les exceptions d'une application spécifique? (il n'est peut-être utile que pour certains types d'applications spécifiques).
Yaroslav Nikitenko
J'ai trouvé un bon article sur ce sujet, julien.danjou.info/python-exceptions-guide . Je pense que les exceptions devraient être sous-classées principalement par domaine, et non par application. Lorsque votre application concerne le protocole HTTP, vous dérivez de HTTPError. Lorsqu'une partie de votre application est TCP, vous dérivez les exceptions de cette partie de TCPError. Mais si votre application couvre un grand nombre de domaines (fichier, autorisations, etc.), la raison d'avoir une MyBaseException diminue. Ou est-ce pour se protéger d'une «violation de couche»?
Yaroslav Nikitenko
6

Voir un très bon article " Le guide définitif des exceptions Python ". Les principes de base sont les suivants:

  • Héritez toujours de (au moins) Exception.
  • Appelez toujours BaseException.__init__avec un seul argument.
  • Lors de la création d'une bibliothèque, définissez une classe de base héritant d'Exception.
  • Fournissez des détails sur l'erreur.
  • Héritez des types d'exceptions intégrés lorsque cela a du sens.

Il y a aussi des informations sur l'organisation (en modules) et le wrapping des exceptions, je recommande de lire le guide.

Yaroslav Nikitenko
la source
1
C'est un bon exemple de la raison pour laquelle je vérifie généralement la réponse la plus élevée, mais aussi la plus récente. Ajout utile, merci.
logicOnAbstractions
1
Always call BaseException.__init__ with only one argument.Semble comme une contrainte inutile, car elle accepte en fait un nombre illimité d'arguments.
Eugene Yarmash
@EugeneYarmash Je suis d'accord, maintenant je ne comprends pas cela. Je ne l'utilise pas de toute façon. Je devrais peut-être relire l'article et développer ma réponse.
Yaroslav Nikitenko
@EugeneYarmash J'ai relu l'article. Il est indiqué qu'en cas de plusieurs arguments, l'implémentation C appelle "return PyObject_Str (self-> args);" Cela signifie qu'une chaîne devrait fonctionner mieux que plusieurs. Avez-vous vérifié cela?
Yaroslav Nikitenko
3

Essayez cet exemple

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")
Omkaar.K
la source
1

Pour définir correctement vos propres exceptions, vous devez suivre quelques bonnes pratiques:

  • Définissez une classe de base héritant de Exception. Cela permettra d'attraper toutes les exceptions liées au projet (des exceptions plus spécifiques devraient en hériter):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Organiser ces classes d'exception dans un module séparé (par exemple exceptions.py) est généralement une bonne idée.

  • Pour créer une exception personnalisée, sous-classe la classe d'exception de base.

  • Pour ajouter la prise en charge des arguments supplémentaires à une exception personnalisée, définissez une __init__()méthode personnalisée avec un nombre variable d'arguments. Appelez la classe de base en lui __init__()passant tous les arguments positionnels (rappelez-vous que BaseException/Exception attendez - vous à n'importe quel nombre d' arguments positionnels ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Pour déclencher une telle exception avec un argument supplémentaire, vous pouvez utiliser:

    raise CustomError('Something bad happened', foo='foo')

Cette conception respecte le principe de substitution Liskov , car vous pouvez remplacer une instance d'une classe d'exception de base par une instance d'une classe d'exception dérivée. En outre, il vous permet de créer une instance d'une classe dérivée avec les mêmes paramètres que le parent.

Eugene Yarmash
la source