web-dev-qa-db-fra.com

Manière correcte de déclarer des exceptions personnalisées dans Python moderne?

Quelle est la bonne façon de déclarer des classes d'exception personnalisées dans Python moderne? Mon objectif principal est de respecter la norme des autres classes d’exception, afin que, par exemple, toute chaîne supplémentaire que j’inclue dans l’exception soit imprimée par l’outil pris en compte par l’exception.

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

L'avertissement de désapprobation suivant dans Python 2.6.2 m'a déclenché:

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

Il semble fou que BaseException ait une signification particulière pour les attributs nommés message. D'après ce que je comprends de PEP-352 , cet attribut avait une signification particulière dans la version 2.5, il essayait de le rendre obsolète. Je suppose donc que ce nom (et celui-là seul) est maintenant interdit? Pouah.

Je suis aussi conscient du fait que Exception a un paramètre magique args, mais je n’ai jamais su l’utiliser. Je ne suis pas sûr non plus que ce soit la bonne façon de faire les choses; la plupart des discussions que j'ai trouvées en ligne ont suggéré qu'ils essayaient de supprimer les arguments dans Python 3.

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

1130
Nelson

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

class MyException(Exception):
    pass

Edit: 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 passer les messages d'erreur au second paramètre et y accéder plus tard avec e.errors


Mise à jour de 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
1170
gahooa

Avec les exceptions Python modernes, vous n'avez pas besoin d'abuser de .message, ni de remplacer .__str__() ou .__repr__() ou aucun de ses éléments. Si tout ce que vous voulez, c'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 une trace se terminant par MyException: My hovercraft is full of eels.

Si vous souhaitez plus de flexibilité à partir de l'exception, vous pouvez utiliser un dictionnaire comme argument:

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

Cependant, obtenir ces détails dans un bloc except est un peu plus compliqué. Les détails sont stockés dans l'attribut args, qui est une liste. Vous devriez faire quelque chose comme ça:

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 Tuple, mais cela est fortement déconseillé (et était même destiné à la dépréciation il y a longtemps). Si vous avez besoin de plus d’un élément d’information et que la méthode ci-dessus ne vous suffit pas, vous devez alors placer la sous-classe Exception comme décrit dans le tutoriel .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
445
frnknstn

"Moyen approprié de déclarer des exceptions personnalisées dans Python moderne?"

C'est 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 pass donnez une docstring:

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

Sous-classes d'exceptions

De la docs

Exception

Toutes les exceptions intégrées, ne sortant 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 if 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 Si vous dérivez toujours de Exception comme le recommande la documentation). En outre, vous pouvez au moins fournir une docstring (et ne pas être obligé d'utiliser le mot clé pass]:

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

Définissez les attributs que vous créez vous-même avec un __init__ personnalisé. Évitez de passer un dict comme argument de position, les futurs utilisateurs de votre code vous en remercieront. Si vous utilisez l'attribut obsolète de message, l'attribuer vous-même évitera un 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__. Les composants internes sont très gentils, 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) ou attraper Exception (mais vous n'êtes probablement pas prêt à gérer tous les types d'exceptions, et vous ne devriez attraper que les exceptions que vous êtes prêt à gérer). Des critiques similaires à celles décrites ci-dessous, mais ce n’est pas non plus le moyen d’initialiser via super, et vous obtiendrez un DeprecationWarning si vous accédez à l’attribut du message:

Edit: 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 pourrez passer les messages d'erreur au second paramètre et y accéder plus tard avec e.errors

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

Pour être direct - cela viole 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'
178
Aaron Hall

voyez comment les exceptions fonctionnent par défaut si un vs plusieurs attributs sont utilisés (tracebacks omitted):

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

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

vous voudrez peut-être une sorte de "modèle d'exception", fonctionnant comme une exception elle-même, de 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 semblable à un tuple, ajoutez simplement la méthode __str__ à la classe ExceptionTemplate, comme suit:

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

et vous aurez

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

Vous devez remplacer les méthodes __repr__ ou __unicode__ au lieu d'utiliser message. Les arguments que vous fournissez lorsque vous construisez l'exception seront dans l'attribut args de l'objet exception.

17
M. Utku ALTINKAYA

À partir de 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 solution 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 chercher 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=None est important pour le rendre décapable. Avant de le jeter, vous devez appeler error.__reduce__(). Le chargement fonctionnera comme prévu.

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

12
fameman

Non, le "message" n'est pas interdit. C'est juste obsolète. Votre application fonctionnera bien avec using 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, bon nombre d'entre elles ne sous-classent pas uniquement d'exception, mais d'autres, telles que ValueError ou similaire. Ensuite, vous devez vous adapter à leur utilisation des variables.

Et si votre application comporte de nombreuses exceptions, il est généralement préférable d’avoir une classe de base personnalisée commune pour toutes les applications, afin que les utilisateurs de vos modules puissent le faire.

try:
    ...
except NelsonsExceptions:
    ...

Et dans ce cas, vous pouvez effectuer le __init__ and __str__ nécessaire, sans avoir à le répéter à chaque exception. Mais le simple fait d'appeler la variable message autrement que message fait l'affaire.

Dans tous les cas, vous n'avez besoin du __init__ or __str__ que si vous faites quelque chose de différent de ce que fait Exception. 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. ;)

7
Lennart Regebro

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")
2
Omkaar.K

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

  • Toujours hériter de (au moins) Exception.
  • Appelez toujours BaseException.__init__ avec un seul argument.
  • Lors de la construction 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 vous semble judicieux.

Il existe également des informations sur l'organisation (dans les modules) et le wrapping des exceptions, je vous recommande de lire le guide.

1
Yaroslav Nikitenko