Bonjour,
Mon problème initial était de fournir à une calculatrice des fonctions de calcul, mais au cas où l'utilisateur fournissait de mauvaises données, par exemple une chaîne au lieu d'un nombre entier, je voulais que chaque fonction concernée renvoie un message d'erreur explicite pour permettre une correction facile. Il faut en effet absolument éviter en cas d'erreur de données:
- un message d'erreur tellement général qu'on ne sait pas quoi en faire
- pire: un crash du programme sans aucun message
- encore pire: un résultat faux sans aucune mention de l'erreur de données
Mais avant, je devais coder plein de tests "if isinstance(...) raise..." au début de chaque fonction, et ça me semblait pénible (une sorte de pollution du code...). J'ai donc cherché une solution plus pratique.
J'ai donc fabriqué un décorateur sous forme d'une classe avec arguments, et dont les arguments sont les obligations de type portant sur les arguments de la fonction décorée.
Par exemple, pour une fonction qui calcule les mensualités d'un crédit (le code complet est plus loin) avec C=capital emprunté, IA=intérêt annuel en %, N=nombre de mois de remboursement
Voilà le décorateur en version Python 3 (chez moi: v3.7). Je l'ai mis sous forme d'une classe "veriftypes" dans un fichier "veriftypes.py" à importer dans les programmes développés:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 @veriftypes(C=(int,), IA=(int, float), N=(int,)) def menscredit(C, IA, N=12): ... ...
En fait, la complexité vient du fait qu'il faut retrouver tous les arguments de la définition de la fonction, et leur affecter toutes les valeurs citées ou non dans l'appel. Et il y a des cas de figures complexes.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75 # -*- coding: utf-8 -*- # Python 3.7 # le décorateur "wraps" permettra à la fonction ou la méthode décorée de # conserver son nom ("__name__") et son docstring ("__doc__"): from functools import wraps ############################################################################## class veriftypes: """Décorateur pour vérification des types des arguments d'une fonction ou d'une méthode décorée (ne fonctionne pas avec des arguments comme *args ou **kwargs) """ #========================================================================= def __init__(self, **deckwargs): """initialisation du décorateur """ # dictionnaire des "variable:type(s)" pour les arguments à tester self.deckwargs = deckwargs #========================================================================= def __call__(self, fonc): #--------------------------------------------------------------------- @wraps(fonc) def appelfonc(*args, **kwargs): """méthode exécutée à chaque appel de la fonction décorée """ # dico de toutes les "variable:valeur" passées à la fonction décorée dicvars = dict(zip(lvars[:len(args)],args)) dicvars.update(kwargs) for v in dvars_def: if v not in dicvars: # ajout des variables par défaut non données avec leur valeur dicvars[v] = dvars_def[v] # vérif. des types pour chaque var. par défaut passée au décorateur for decvar in self.deckwargs: # vérif. que l'argument du décorateur existe dans les arguments de la fonction décorée if decvar not in lvars: raise ValueError ('''Erreur veriftypes sur "{}": la variable "{}" n'existe pas'''.format(nomfonc, decvar)) # vérification de type(s) pour l'argument passé au décorateur if decvar in dicvars: # valeur passée à la fonction décorée pour l'argument val = dicvars[decvar] # type(s) déclaré(s) au décorateur typ = self.deckwargs[decvar] # vérification if not isinstance(val, typ): raise TypeError ('Erreur appel "{}": mauvais type pour "{}"'.format(nomfonc, decvar)) # appel de fonc avec tous ses arguments, et retour du résultat return fonc(*args, **kwargs) #--------------------------------------------------------------------- # nom de la fonction décorée pour les messages suite à exception nomfonc = fonc.__name__ # liste de toutes les variables d'appel de la fonction décorée lvars = list(fonc.__code__.co_varnames[:fonc.__code__.co_argcount]) # liste des valeurs par defaut de la fonction décorée args_def = fonc.__defaults__ if args_def == None: args_def = () # aucune variable par défaut n'est déclarée # liste des variables par défaut vars_def = lvars[len(lvars)-len(args_def):] # création du dictionnaire des "variable:valeur" par défaut dvars_def = dict(zip(vars_def, args_def)) # retourne l'adresse de la méthode à appeler à chaque appel de fonc return appelfonc
Par exemple, j'ai la définition: "def menscredit(C, IA, N=12):" Je peux appeler:
- menscredit(1000, 5.25) => N sera 12 par défaut
- menscredit(1000, 5.25, 24) => N sera 24 mais ici, N=24 est cité comme un argument par position
- menscredit(1000, IA=5.25, N=24) => IA sera 5.25, mais IA était un argument par position et il est appelé comme un argument par défaut
- etc...
Et, bien sûr, pour que la vérification du type d'un argument soit faite, il faut que le nom de cet argument soit cité dans la définition de la fonction! Cela exclut que ça puisse marcher avec une fonction comme "def test(*args, **kwargs):"
Voilà un exemple de fonction décorée:
Et voilà ce qui se passe lors de l'appel de cette fonction:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 # -*- coding: utf-8 -*- # Python 3.7 """ Exemple de vérification de types d'arguments d'une fonction par décorateur """ from veriftypes import veriftypes ############################################################################### @veriftypes(C=(int,), IA=(int, float), N=(int,)) def menscredit(C, IA, N=12): """Retoune la mensualité M d'un prêt C à IA% d'intérêt par an, à rembourser en N mois """ # si le capital est nul, il n'y a rien à rembourser if C==0: return 0 # si l'intérêt est nul, la mensualité ne dépend plus que de C et N if IA==0: return C/N # calcul de la mensualité dans les autres cas I = IA/1200 return C*I*(1-1/(1-(1+I)**N))
- menscredit(1000, 5.25, 24) => 43.98
- menscredit(1000, 5.25, "24") => Erreur appel "menscredit": mauvais type pour "N"
- menscredit(1000.0, 5.25, 24) => Erreur appel "menscredit": mauvais type pour "C"
- etc...
Voilà comment fonctionne ce décorateur:
- lors de la 1ère exécution du code, le décorateur se configure pour la fonction qu'il décore. La méthode __init__ sauvegarde les arguments passés au décorateur, puis la méthode __call__ cherche toutes les caractéristiques des arguments passés dans la définition de la fonction (lignes 59 à 75 de veriftypes.py), et retourne l'adresse de la méthode appelfonc.
- lors des exécutions suivantes, seule la méthode appelfonc sera appelée à chaque appel de la fonction décorée, ce qui permettra de traiter la correspondance entre les arguments appelés et les arguments prévus à la définition de la fonction.
A noter que ça marche aussi pour décorer la méthode d'une classe.
A noter aussi que ça continue à marcher pour une version "standalone" après traitement par pyinstaller ou cx_freeze. Et c'est normal, parce qu'il ne s'agit pas d'une compilation en code natif comme en C, mais d'une "encapsulation" de l'interpréteur + les bibliothèques et modules nécessaires.
On peut faire encore mieux: ajouter des conditions sur la valeur des arguments passés! Le décorateur ci-dessus se complète par quelques lignes de code pour faire ça. Bien sûr, ce décorateur fait aussi bien que le précédent la vérification des types!
Par exemple, toujours pour ma fonction de calcul de crédit:
Pour faire ça, il a seulement fallu:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 @verifargs("C>=0", "IA>=0.0", "N>0", C=(int,), IA=(int, float), N=(int,)) def menscredit(C, IA, N=12):
- ajouter des arguments par position au décorateur
- vérifier par eval la satisfaction de chacune des conditions, avec la valeur transmise lors de l'appel de la fonction.
Voilà ce nouveau décorateur complété. La classe est nommée maintenant "verifargs" et se trouve dans un fichier "verifargs.py qu'il faudra importer:
Maintenant, si on appelle "menscredit(1000, 5.25, -24)", donc avec un nombre de mois négatif, cela donne: "Erreur appel "menscredit": échec condition "N>0""
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 # -*- coding: utf-8 -*- # Python 3.7 # le décorateur "wraps" permettra à la fonction ou la méthode décorée de # conserver son nom ("__name__") et son docstring ("__doc__"): from functools import wraps ############################################################################## class verifargs: """Décorateur pour vérification des types des arguments d'une fonction ou d'une méthode décorée (ne fonctionne pas avec des arguments comme *args ou **kwargs) """ #========================================================================= def __init__(self, *decargs, **deckwargs): """initialisation du décorateur """ # arg. par position: liste des conditions à remplir self.decargs = decargs # dictionnaire des "variable:type(s)" pour les arguments à tester self.deckwargs = deckwargs #========================================================================= def __call__(self, fonc): #--------------------------------------------------------------------- @wraps(fonc) def appelfonc(*args, **kwargs): """méthode exécutée à chaque appel de la fonction décorée """ # dico de toutes les "variable:valeur" passées à la fonction décorée dicvars = dict(zip(lvars[:len(args)],args)) dicvars.update(kwargs) for v in dvars_def: if v not in dicvars: # ajout des variables par défaut non données avec leur valeur dicvars[v] = dvars_def[v] # vérif. des types pour chaque var. par défaut passée au décorateur for decvar in self.deckwargs: # vérif. que l'argument du décorateur existe dans les arguments de la fonction décorée if decvar not in lvars: raise ValueError ('''Erreur veriftypes sur "{}": la variable "{}" n'existe pas'''.format(nomfonc, decvar)) # vérification de type(s) pour l'argument passé au décorateur if decvar in dicvars: # valeur passée à la fonction décorée pour l'argument val = dicvars[decvar] # type(s) déclaré(s) au décorateur typ = self.deckwargs[decvar] # vérification if not isinstance(val, typ): raise TypeError ('Erreur appel "{}": mauvais type pour "{}"'.format(nomfonc, decvar)) # vérif. des conditions à remplir sur les arguments par position du décorateur for decarg in self.decargs: if not eval(decarg, dicvars): raise ValueError ('Erreur appel "{}": échec condition "{}"'.format(nomfonc, decarg)) # appel de fonc avec tous ses arguments, et retour du résultat return fonc(*args, **kwargs) #--------------------------------------------------------------------- # nom de la fonction décorée pour les messages suite à exception nomfonc = fonc.__name__ # liste de toutes les variables d'appel de la fonction décorée lvars = list(fonc.__code__.co_varnames[:fonc.__code__.co_argcount]) # liste des valeurs par defaut de la fonction décorée args_def = fonc.__defaults__ if args_def == None: args_def = () # aucune variable par défaut n'est déclarée # liste des variables par défaut vars_def = lvars[len(lvars)-len(args_def):] # création du dictionnaire des "variable:valeur" par défaut dvars_def = dict(zip(vars_def, args_def)) # retourne l'adresse de la méthode à appeler à chaque appel de fonc return appelfonc
A noter que, comme eval utilise le dictionnaire des arguments de la fonction, chacune des conditions peut être plus complexe, et concerner plusieurs arguments. Par exemple, une condition "C>=0 and C>N" est parfaitement valable (même si elle est absurde dans ce cas).
A noter aussi que si "eval" est déconseillée en général pour des raisons de sécurité (et à juste titre), ce n'est pas un problème ici puisque les chaînes de caractères à évaluer sont uniquement dans le code du programme.
Ces vérifications de type ne sont pas à utiliser tout le temps pour chaque fonction ou méthode, mais elles sont particulièrement utiles pendant le développement, ainsi que pendant l'utilisation, dès lors qu'on risque d'introduire de mauvaises données. En fait, on l'utilise dès qu'on trouverait nécessaire d'ajouter des tests "if...raise..." au début des fonctions concernées.
N'hésitez pas à me signaler les éventuels problèmes que vous rencontrez dans l'utilisation!
Partager