Refactoriser ses vérifications en Python


On nous a interpelé sur Twitter pour nous demander comment faire un code comme ceci (en tout cas similaire) plus propre:

# -*- coding: utf-8 -*
 
from __future__ import unicode_literals
 
import random
 
def verification1(val):
    return bool(random.randint(0, 1))
 
verification2 = verification1
verification3 = verification1
 
def function(valeur):
 
    if not verification1(valeur):
        print("Erreur, la vérification 1 n'est pas passée")
 
    if not verification2(valeur):
        print("Erreur, la vérification 2 n'est pas passée")
 
    if not verification3(valeur):
        print("Erreur, la vérification 3 n'est pas passée")
 
    return valeur

Ceci est évidement un exemple tout bidon, mais la problématique, c’est qu’on a plein de vérifications à faire, et un code à lancer si jamais ces vérifications échouent. Et chaîner les if, c’est vrai que c’est pas super mignon (même si c’est tout à fait valide, faut pas non plus se prendre trop la tête avec ça).

En Python, on a plusieurs manières de s’occuper de ça. La première, j’en ai déjà parlé, c’est l’injection de dépendances. On va passer ces checks en paramètres.

Car je le rappelle, on peut passer des fonctions en paramètres, on peut mettre des fonctions dans des listes, et on peut même passer des listes de fonctions en paramètres. Ce qui ici va nous permettre de :

  • Regrouper les traitements.
  • Permettre de changer les traitements à la volée.
  • Avoir malgré tout un traitement par défaut.
  • Rendre les groupes de vérifications réutilisables
 
 
VERIF = (
    (verification1, "Erreur, la vérification 1 n'est pas passée"),
    (verification2, "Erreur, la vérification 2 n'est pas passée"),
    (verification3, "Erreur, la vérification 3 n'est pas passée"),
    ((lambda v: not bool(random.randint(0, 1))),
    "Erreur, la vérification 4 n'est pas passée")
)
 
def faire_verifications(verifications):
    for verif, msg in verifications:
        if not verif(valeur):
            print msg
 
def function(valeur, verifications=VERIF):
    faire_verifications(verifications)
    return valeur

Cette manière de faire est plus verbeuse si votre liste de if est courte, ou si vous n’avez à le faire que pour une fonction. Mais elle devient vite plus courte et clair dans le cas où votre code grandi.

Elle aussi l’avantage de pouvoir insérer ou retirer une modification très simplement (même avec une simple lambda). Enfin, le code de vérification est découplé du code de la fonction ce qui ajoute les bénéfices suivant :

  • Votre code de vérification peut être déplacé dans un module dédié.
  • Si vous avez des vérifications en plusieurs endroits, il n’y a qu’un seul endroit pour corriger les bugs des vérifications, et un seul pour faire une modification des vérifications.
  • Le code de la fonction redevient simple : elle fait une vérification (on s’en branle de comment ça marche, on sait que ça vérifie), et ensuite elle s’occupe de SON boulot. C’est la partie du code qui nous intéresse quand on va voir la fonction.

On peut évidement imaginer autre chose qu’un print: lever une exception, faire un log, ou même carrément aussi passer une fonction en lieu et place du message d’erreur qui fait office de callback quand la vérification échoue (et qui par défaut print, le meilleur des deux mondes).

Si vos vérifications deviennent très courantes, alors, l’utilisation de décorateurs prend tout son sens. Je ne vais pas rentrer sur comment écrire son décorateur, il y a un autre article pour ça, mais on peut obtenir un résultat du genre :

def verif(verification, message):
    def decorateur(func):
        def wrapper(*args, **kwargs):
            if not verification(*args, **kwargs):
                print message
            return func(*args, **kwargs)
        return wrapper
    return decorateur
 
# si on utilise souvent une vérif, on peut l'avoir 
# tout le temps tout la main
verif3 = verif(verification3, "Erreur, la vérification 3 n'est pas passée")
 
# et ensuite il suffit d'appliquer autant de décorateur qu'on le souhaite
@verif(lambda v: not bool(random.randint(0, 1)), "Erreur, la vérification 4 n'est pas passée")
@verif3
@verif(verification2, "Erreur, la vérification 2 n'est pas passée")
@verif(verification1, "Erreur, la vérification 1 n'est pas passée")
def function(valeur):
    return valeur

Les avantages ici sont :

  • Corps de la fonction initial vide de toute vérification. Les vérifications sont purement déclaratives (comme par exemple la vérification de login dans Django).
  • Chaque vérif est très facilement réutilisable, il suffit de mettre le décorateur. Si on utilise une vérif très souvent, alors on peut même mettre le décorateur dans une variable et juste déclarer @ma_verif.
  • C’est très pythonique comme style.

En résumé : si vous avez un petit code, ne vous prenez pas la tête. Une série de if est tout à fait valide, ça marche bien, c’est clair et lisible. Il faut savoir aller au plus simple. Mais si votre code devient touffu, utiliser une liste de fonction peut aider à faire le ménage. Si vous commencer carrément à avoir un gros projet ou que vous codez une lib réutilisable, les décorateurs seront de précieux alliés : ils permettent à celui qui passe derrière vous de se foutre complètement de savoir comment ça marche et de juste assembler son code comme un légo.


Télécharger le code de l’article

5 thoughts on “Refactoriser ses vérifications en Python

  • Mentat

    Je pense que là :
    def faire_verifications(verifications):
    on a besoin de passer valeur en argument.

    Non ?

  • Sam Post author

    Anéfé. En plus la coquille n’est pas dans le code à télécharger. Je fais maintenant des erreurs conditionnelles. J’innove.

  • Siltaar

    « vite plus courte et clair dans le cas où votre code grandi. »

    –> et claire (avec un ‘e’ en plus)

    « Elle aussi l’avantage de pouvoir »

    –> Elle a aussi (avec le mot ‘a’ en plus)

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.