angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Créez vos propres classes d'exception Python adaptées à votre application

Une bonne gestion des exceptions vous permet de lire le code plus facilement, mais vous oblige également à réfléchir très soigneusement à ce que vous voulez faire.

17 juin 2020
Dans Python
post main image
https://unsplash.com/@surface

Utiliser les exceptions dans Python semble facile mais ce n'est pas le cas. Vous devriez probablement étudier les exceptions et le traitement des exceptions avant d'écrire un code Python mais TL;DR. Il existe des exemples sur Internet, malheureusement la plupart sont très triviaux. Quoi qu'il en soit, j'ai fait des recherches et j'ai trouvé un code que je pense partager avec vous. Laissez un commentaire si vous avez des suggestions.

Qu'est-ce qu'une erreur et qu'est-ce qu'une exception, quelle est la différence ? Dans Python , une exception est levée lorsqu'une erreur se produit pendant l'exécution du programme, par exemple une ValueError ou une ZeroDivisionError.

Les avantages des exceptions par rapport aux retours d'état peuvent être résumés comme suit : voir aussi le lien ci-dessous "Pourquoi est-il préférable de lancer une exception plutôt que de renvoyer un code d'erreur ?

  1. Les exceptions laissent votre code propre de tous les contrôles nécessaires lorsque le statut de test revient à chaque appel
  2. Les exceptions vous permettent d'utiliser la valeur de retour des fonctions pour les valeurs réelles
  3. Le plus important : les exceptions ne peuvent pas être ignorées par l'inaction, tandis que les retours de statut peuvent

Tous les gens ne sont pas aussi positifs, mais je ne vais pas déclencher une guerre ici.

Quelque part dans notre code, nous pouvons avoir un gestionnaire d'exception qui peut être utilisé, par exemple, pour annuler une insertion de base de données. Nous pouvons également laisser la bulle d'exception se placer en haut du code le plus élevé et présenter une erreur au user.

Enregistrement et paramètres

Si quelque chose se produit, nous devons nous assurer que nous enregistrons l'erreur. Pour faciliter la localisation et la résolution d'un problème (de codage), nous mettons autant d'informations que possible dans notre journal. J'ai décidé que je voulais que les informations suivantes figurent dans le journal :

  • une enquête de traçage
  • les paramètres suivants (optionnels) :
    • e
      C'est la valeur (souvent) renvoyée par une exception
    • code
      Il peut s'agir de notre propre code d'erreur
    • message
      C'est le message que nous voulons montrer au user (visiteur du site web, API user)
    • détails
      Plus d'informations qui peuvent être utiles au user, par exemple les paramètres fournis
    • fargs
      Ce sont les arguments de la fonction où l'exception s'est produite, utiles pour le débogage

Un exemple

Supposons que nous créions une application qui utilise une classe de base de données et une classe de serveur imap. Nous créons notre gestionnaire d'exceptions personnalisé. Il est de bonne pratique de l'avoir dans un fichier exceptions.py que nous importons dans notre application :

# file: exceptions.py

class AppError(Exception):

    def __init__(self, e=None, code=None, message=None, details=None, fargs=None):
        self.e = e
        self.code = code
        self.message = message
        self.details = details
        self.fargs = fargs

    def get_e(self):
        return self.e

    def get_code(self):
        return self.code

    def get_message(self):
        return self.message

    def get_details(self):
        return self.details

    def __str__(self):
        s_items = []
        if self.e is not  None:
            s_items.append('e = {}'.format(self.e))
        if self.code is not  None:
            s_items.append('code = {}'.format(self.code))
        if self.message is not  None:
            s_items.append('message = {}'.format(self.message))
        if self.details is not  None:
            s_items.append('details = {}'.format(self.details))
        if self.fargs is not  None:
            s_items.append('fargs = {}'.format(self.fargs))
        return ', '.join(s_items)


class DbError(AppError):
    pass


class IMAPServerError(AppError):
    pass

Cela ressemble beaucoup à la création de classes normales. Dans ce cas, nous passons zéro ou plus d'arguments nommés à l'exception. Nous avons besoin de la méthode __str__() pour permettre à Python de renvoyer une chaîne avec les données disponibles qui seront stockées dans le fichier app.log. Si nous omettons __str__(), alors app.log ne s'affiche que : exceptions.IMAPServerError".

Le code de l'application :

# file: app.py
 
from exceptions import AppError, DbError, IMAPServerError

import logging

logging.basicConfig(
    format='%(asctime)s - %(levelname)s: %(message)s',
    filename='app.log',
    level=logging.DEBUG
    #level=logging.INFO
)

class A:

    def do_a(self, a):
        # connect to remote system
        b = B()
        b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })

        # do something else


class B:

    def do_b(self, a, b=None, c=None):
        fname = 'B.do_b'
        fargs = locals()

        abc = 'not in locals'

        # check values
        if b not in [8, 16]:
            raise IMAPServerError(
                fargs = fargs,
                e = 'Input must be 8 or 16',
                message = 'Input must be 8 or 16, value supplied: {}'.format(b)
            )

        # connect to remote system
        try:
            # simulate something went wrong
            # raise IMAPServerError('error 123')
            if b == 8:
                d = b/0

        except (ZeroDivisionError, IMAPServerError) as e:
            raise IMAPServerError(
                fargs = fargs,
                e = e,
                message = 'Connection to remote system failed. Please check your settings',
                details = 'Connection parameters:  username = John'
            ) from e


def run(i):

    a = A()
    a.do_a(i)


def do_run():

    # 7: input error
    # 8: connection error
    # 16: no error
    for i in [7, 8, 16]:

        print('Run with i = {}'.format(i))
        try:
            run(i)
            print('No error(s)')

        except (DbError, IMAPServerError) as e:
            logging.exception('Stack trace')
            print('Error: {}'.format(e.message))
            print('Details: {}'.format(e.details))

    print('Ready')


if __name__ == '__main__':
    do_run()

Notez que les exceptions sont enregistrées dans un fichier app.log en utilisant "logging.exception". Nous utilisons également la fonction Python locals() pour capturer les arguments de la fonction ou de la méthode où l'exception s'est produite. Dans l'exception "connect to remote system", nous remontons avec tous les paramètres que nous voulons dans le journal. Enfin, nous utilisons ici la construction "raise ... from e". Cela révèle l'erreur de division zéro comme étant la cause initiale.

Pour exécuter, tapez :

python3 app.py

Le résultat est :

Run with i = 7
Error: Input must be 8 or 16, value supplied: 7
Details:  None
Run with i = 8
Error: Connection to remote system failed. Please check your settings
Details: Connection parameters:  username = John
Run with i = 16
No error(s)
Ready

et le fichier journal app.log :

2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 38, in do_b
    message = 'Input must be 8 or 16, value supplied: {}'.format(b)
exceptions.IMAPServerError: e = Input must be 8 or 16, message = Input must be 8 or 16, value supplied: 7, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 7, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}
2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 46, in do_b
    d = b/0
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 53, in do_b
    ) from e
exceptions.IMAPServerError: e = division by zero, message = Connection to remote system failed. Please check your settings, details = Connection parameters:  username = John, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 8, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}

app.log contient deux entrées d'ERREUR. Une pour l'erreur "L'entrée doit être 8 ou 16" et une pour l'erreur "La connexion au système à distance a échoué".

Résumé

La vie pourrait être simple, mais elle ne l'est pas. Une bonne gestion des exceptions vous donne un code plus facile à lire mais vous oblige aussi à réfléchir très soigneusement à ce que vous voulez faire. Ce serait bien si nous pouvions aussi ajouter les arguments des fonctions appelées menant à la fonction soulevant l'exception mais c'est plus difficile.

Liens / crédits

How to get value of arguments passed to functions on the stack?
https://stackoverflow.com/questions/6061744/how-to-get-value-of-arguments-passed-to-functions-on-the-stack

Proper way to declare custom exceptions in modern Python?
https://stackoverflow.com/questions/1319615/proper-way-to-declare-custom-exceptions-in-modern-python

The Most Diabolical Python Antipattern
https://realpython.com/the-most-diabolical-python-antipattern/

Why Exceptions Suck (ckwop.me.uk)
https://news.ycombinator.com/item?id=232890

Why is it better to throw an exception rather than return an error code?
https://stackoverflow.com/questions/4670987/why-is-it-better-to-throw-an-exception-rather-than-return-an-error-code

Your API sucks (so I’ll catch all exceptions)
https://dorinlazar.ro/your-api-sucks-catch-exceptions/

En savoir plus...

Exceptions

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.