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.
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 ?
- Les exceptions laissent votre code propre de tous les contrôles nécessaires lorsque le statut de test revient à chaque appel
- Les exceptions vous permettent d'utiliser la valeur de retour des fonctions pour les valeurs réelles
- 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
- e
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
Récent
- Masquer les clés primaires de la base de données UUID de votre application web
- Don't Repeat Yourself (DRY) avec Jinja2
- SQLAlchemy, PostgreSQL, nombre maximal de lignes par user
- Afficher les valeurs des filtres dynamiques SQLAlchemy
- Transfert de données sécurisé grâce au cryptage à Public Key et à pyNaCl
- rqlite : une alternative à haute disponibilité et dist distribuée SQLite
Les plus consultés
- Utilisation des Python's pyOpenSSL pour vérifier les certificats SSL téléchargés d'un hôte
- Utiliser UUIDs au lieu de Integer Autoincrement Primary Keys avec SQLAlchemy et MariaDb
- Utiliser PyInstaller et Cython pour créer un exécutable Python
- Connexion à un service sur un hôte Docker à partir d'un conteneur Docker
- SQLAlchemy : Utilisation de Cascade Deletes pour supprimer des objets connexes
- Flask RESTful API validation des paramètres de la requête avec les schémas Marshmallow