Python est un langage de programmation idéal pour développer des scripts autonomes. Pour obtenir le résultat souhaité à l'aide d'un script similaire, vous devez écrire plusieurs dizaines ou centaines de lignes de code. Et une fois le travail terminé, vous pouvez simplement oublier le code écrit et passer à la solution du problème suivant.
Si, par exemple, six mois après l'écriture d'un certain script «à usage unique», quelqu'un demande à l'auteur pourquoi ce script échoue, l'auteur du script peut ne pas être au courant de cela. Cela se produit car le script n'a pas été une documentation écrite, en raison de l'utilisation de paramètres codés en dur dans le code, en raison du fait que le script n'enregistre rien pendant le travail et en raison du manque de tests qui ont permis pour comprendre rapidement la cause du problème.

Il convient de noter que transformer un script écrit à la hâte en quelque chose de beaucoup mieux n'est pas si difficile. À savoir, un tel script est assez facile à transformer en un code fiable et compréhensible qui est pratique à utiliser, en un code qui est simple à prendre en charge à la fois son auteur et d'autres programmeurs.
L'auteur du matériel, dont nous publions la traduction aujourd'hui, va démontrer une telle «transformation» en utilisant le classique
Fizz Buzz Test comme exemple. Cette tâche consiste à afficher une liste de nombres de 1 à 100, en remplaçant certains d'entre eux par des lignes spéciales. Donc, si le nombre est un multiple de 3 - au lieu de cela, vous devez imprimer la chaîne
Fizz
, si le nombre est un multiple de 5 - la chaîne
Buzz
, et si ces deux conditions sont remplies -
FizzBuzz
.
Code source
Voici le code source d'un script Python qui résout le problème:
import sys for n in range(int(sys.argv[1]), int(sys.argv[2])): if n % 3 == 0 and n % 5 == 0: print("fizzbuzz") elif n % 3 == 0: print("fizz") elif n % 5 == 0: print("buzz") else: print(n)
Parlons de la façon de l'améliorer.
La documentation
Je trouve utile d'écrire de la documentation avant d'écrire du code. Cela simplifie le travail et permet de ne pas retarder indéfiniment la création de documentation. La documentation du script peut être placée en haut. Par exemple, cela pourrait ressembler à ceci:
La première ligne donne une brève description de l'objectif du script. Les paragraphes restants fournissent des informations supplémentaires sur ce que fait le script.
Arguments de ligne de commande
La prochaine tâche pour améliorer le script consistera à remplacer les valeurs codées en dur dans le code par les valeurs documentées transmises au script via les arguments de ligne de commande. Cela peut être fait en utilisant le module
argparse . Dans notre exemple, nous suggérons à l'utilisateur de spécifier une plage de nombres et de spécifier les valeurs pour "fizz" et "buzz" utilisées lors de la vérification des nombres de la plage spécifiée.
import argparse import sys class CustomFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): pass def parse_args(args=sys.argv[1:]): """Parse arguments.""" parser = argparse.ArgumentParser( description=sys.modules[__name__].__doc__, formatter_class=CustomFormatter) g = parser.add_argument_group("fizzbuzz settings") g.add_argument("--fizz", metavar="N", default=3, type=int, help="Modulo value for fizz") g.add_argument("--buzz", metavar="N", default=5, type=int, help="Modulo value for buzz") parser.add_argument("start", type=int, help="Start value") parser.add_argument("end", type=int, help="End value") return parser.parse_args(args) options = parse_args() for n in range(options.start, options.end + 1):
Ces modifications sont très utiles au script. A savoir, les paramètres sont désormais correctement documentés, vous pouvez découvrir leur fonction en utilisant l'indicateur
--help
. De plus, selon la commande correspondante, la documentation que nous avons écrite dans la section précédente est également affichée:
$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". positional arguments: start Start value end End value optional arguments: -h, --help show this help message and exit fizzbuzz settings: --fizz N Modulo value for fizz (default: 3) --buzz N Modulo value for buzz (default: 5)
Le module
argparse
est un outil très puissant. Si vous ne le connaissez pas, il vous sera utile de consulter sa
documentation . En particulier, j'aime sa capacité à définir des
sous -
commandes et des
groupes d'arguments .
Journalisation
Si vous équipez le script de la possibilité d'afficher certaines informations lors de son exécution, cela s'avérera être un ajout agréable à sa fonctionnalité. Le module d'
enregistrement est bien adapté à cet effet. Tout d'abord, nous décrivons un objet qui implémente la journalisation:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
Ensuite, nous pourrons contrôler les détails des informations affichées lors de la journalisation. Ainsi, la commande
logger.debug()
ne devrait produire quelque chose que si le script est exécuté avec le commutateur
--debug
. Si le script est exécuté avec le
--silent
, le script ne doit rien afficher, à l'exception des messages d'exception. Pour implémenter ces fonctionnalités, ajoutez le code suivant à
parse_args()
:
Ajoutez la fonction suivante au code de projet pour configurer la journalisation:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
Le code de script principal changera comme suit:
if __name__ == "__main__": options = parse_args() setup_logging(options) try: logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) for n in range(options.start, options.end + 1):
Si vous prévoyez d'exécuter le script sans la participation directe de l'utilisateur, par exemple, à l'aide de
crontab
, vous pouvez faire en sorte que sa sortie passe à
syslog
:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: if not sys.stderr.isatty(): facility = logging.handlers.SysLogHandler.LOG_DAEMON sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) sh.setFormatter(logging.Formatter( "{0}[{1}]: %(message)s".format( logger.name, os.getpid()))) root.addHandler(sh) else: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
Dans notre petit script, une quantité similaire de code semble nécessaire pour simplement utiliser la commande
logger.debug()
. Mais dans de vrais scripts, ce code ne ressemblera plus à cela et l'avantage en sera tiré au premier plan, à savoir qu'avec son aide, les utilisateurs pourront se renseigner sur les progrès de la résolution du problème.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
Les tests
Les tests unitaires sont un outil utile pour vérifier si les applications se comportent comme elles le devraient. Les scripts d'unité sont rarement utilisés dans les scripts, mais leur inclusion dans les scripts améliore considérablement la fiabilité du code. Nous transformons le code à l'intérieur de la boucle en fonction et décrivons plusieurs exemples interactifs de son utilisation dans sa documentation:
def fizzbuzz(n, fizz, buzz): """Compute fizzbuzz nth item given modulo values for fizz and buzz. >>> fizzbuzz(5, fizz=3, buzz=5) 'buzz' >>> fizzbuzz(3, fizz=3, buzz=5) 'fizz' >>> fizzbuzz(15, fizz=3, buzz=5) 'fizzbuzz' >>> fizzbuzz(4, fizz=3, buzz=5) 4 >>> fizzbuzz(4, fizz=4, buzz=6) 'fizz' """ if n % fizz == 0 and n % buzz == 0: return "fizzbuzz" if n % fizz == 0: return "fizz" if n % buzz == 0: return "buzz" return n
Vous pouvez vérifier le bon fonctionnement de la fonction à l'aide de
pytest
:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [100%] ========================== 1 passed in 0.05 seconds ==========================
Pour que tout cela fonctionne, vous avez besoin de l'extension
.py
après le nom du script. Je n'aime pas ajouter des extensions aux noms de script: la langue n'est qu'un détail technique qui n'a pas besoin d'être montré à l'utilisateur. Cependant, il semble qu'équiper un nom de script avec une extension est le moyen le plus simple de laisser les systèmes pour exécuter des tests, comme
pytest
, trouver les tests inclus dans le code.
Si une erreur
pytest
affichera un message indiquant l'emplacement du code correspondant et la nature du problème:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz FAILED [100%] ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101 >>> fizzbuzz(5, fizz=3, buzz=5) 102 'buzz' 103 >>> fizzbuzz(3, fizz=3, buzz=5) 104 'fizz' 105 >>> fizzbuzz(15, fizz=3, buzz=5) 106 'fizzbuzz' 107 >>> fizzbuzz(4, fizz=3, buzz=5) 108 4 109 >>> fizzbuzz(4, fizz=4, buzz=6) Expected: fizz Got: 4 /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================
Les tests unitaires peuvent également être écrits sous forme de code standard. Imaginez que nous devons tester la fonction suivante:
def main(options): """Compute a fizzbuzz set of strings and return them as an array.""" logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) return [str(fizzbuzz(i, options.fizz, options.buzz)) for i in range(options.start, options.end+1)]
À la fin du script, nous ajoutons les tests unitaires suivants à l'aide des
pytest
pour l'utilisation
des fonctions de test paramétrées :
Veuillez noter que, puisque le code du script se termine par un appel à
sys.exit()
, les tests ne seront pas exécutés lorsqu'il est appelé normalement. Grâce à cela,
pytest
pas nécessaire pour exécuter le script.
La fonction de test sera appelée une fois pour chaque groupe de paramètres. L'entité
args
est utilisée comme entrée pour la fonction
parse_args()
. Grâce à ce mécanisme, nous obtenons ce dont nous avons besoin pour passer à la fonction
main()
. L'entité
expected
est comparée à ce que
main()
. Voici ce que
pytest
nous dira si tout fonctionne comme prévu:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED [100%] ========================== 7 passed in 0.03 seconds ==========================
En cas d'erreur,
pytest
fournira des informations utiles sur ce qui s'est passé:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [...] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________ args = '0 0', expected = ['0'] @pytest.mark.parametrize("args, expected", [ ("0 0", ["0"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected E AssertionError: assert ['fizzbuzz'] == ['0'] E At index 0 diff: 'fizzbuzz' != '0' E Full diff: E - ['fizzbuzz'] E + ['0'] fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py 125 DEBUG compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================
La sortie de la commande
logger.debug()
est
logger.debug()
incluse dans cette sortie. C'est une autre bonne raison d'utiliser des mécanismes de journalisation dans les scripts. Si vous voulez en savoir plus sur les grandes fonctionnalités de
pytest
, jetez un œil à
ce matériau.
Résumé
Vous pouvez rendre les scripts Python plus fiables en suivant ces quatre étapes:
- Équipez le script de la documentation située en haut du fichier.
- Utilisez le module
argparse
pour documenter les paramètres avec lesquels le script peut être appelé. - Utilisez le module de
logging
pour afficher des informations sur le processus d'opération de script. - Écrire des tests unitaires.
Voici le code complet de l'exemple présenté ici. Vous pouvez l'utiliser comme modèle pour vos propres scripts.
Des discussions intéressantes ont commencé autour de ce matériel - vous pouvez les trouver
ici et
ici . Le public, semble-t-il, a bien reçu des recommandations sur la documentation et sur les arguments de la ligne de commande, mais qu'en est-il de la journalisation et des tests semblait à certains lecteurs être un "tir d'un canon sur des moineaux."
Voici le matériel qui a été écrit en réponse à cet article.
Chers lecteurs! Envisagez-vous d'appliquer les recommandations pour l'écriture de scripts Python données dans cette publication?
