écrire un lanceur / point d’entrée¶
Quand on écrit un projet en Python, on peut publier uniquement une librairie : un paquet de classes, de fonctions, destinées à être utilisées depuis une autre application.
Il est parfois aussi utile de publier un vrai “lanceur”, c’est-à-dire un programme complet, destiné à être lancé en tant que tel avec une commande dans le terminal, genre
$ python script.py
On appelle alors ce fichier script.py
un lanceur, ou encore un point d’entrée
lire les arguments¶
Il est souvent utile de pouvoir passer à ce lanceur des paramètres
Par exemple si vous avez écrit un programme qui calcule la factorielle, on veut pouvoir passer au lanceur l’entier dont on veut calculer la factorielle, en écrivant
$ python factorielle.py 6
factorial(6)=720
Il nous faut donc trouver un moyen de “faire passer” ce paramètre 6
depuis le terminal jusqu’à l’application Python; pour cela on dispose de deux mécanismes :
sys.argv
le module
argparse
De manière générale, sauf dans les cas simplissimes, il est préférable d’utiliser le second, bien qu’un tout petit peu plus complexe que le premier; voyons cela
sys.argv
¶
Python permet de retrouver la commande de lancement du point d’entrée au travers de l’attribu sys.argv
, sous la forme d’une liste de chaines (la commande initiale est découpée selon les espaces)
Considérons pour commencer le programme suivant
# %cat est une 'magic' de IPythion
# qui permet d'afficher le contenu de fact_sysargv.py
%cat fact_sysargv.py
# on importe sys pour pouvoir accéder à sys.argv
import sys
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
# le programme principal est souvent appelé `main`
def main():
# sys.argv est une liste avec d'abord le nom du lanceur
# puis les paramètres de la ligne de commande
print(f"argv={sys.argv}")
# on extrait le premier paramètre
# qui est à l'index 1 car l'index 0 contient le lanceur
number = int(sys.argv[1])
# ya plus ka
print(f"factorial({number})={factorial(number)}")
# un idiome fréquent : pour les points d'entrée
# on n'exécute ceci
# **SEULEMENT** si le fichier est vraiment le point d'entrée,
# et **PAS** s'il est importé
if __name__ == '__main__':
main()
# avec le ! je fais comme si je tapais ça dans le terminal
!python fact_sysargv.py 6
argv=['fact_sysargv.py', '6']
factorial(6)=720
Cette mécanique fonctionne mais présente de gros défauts :
principalement, on ne fait aucun contrôle sur les paramètres; si vous appelez le programme avec trop ou pas assez d’arguments, il se passe des trucs pas forcément très nets
et si vous ne vous souvenez plus de comment il faut appeler le programme, vous en êtes quittes pour retrouver et relire le code…
# par exemple si on essaie de lancer le programme sans le paramètre
# on obtient une erreur - c'est normal -
# mais franchement ce n'est pas très clair de comprendre ce qu'on a mal fait
! python fact_sysargv.py
argv=['fact_sysargv.py']
Traceback (most recent call last):
File "/home/runner/work/python-advanced/python-advanced/notebooks/fact_sysargv.py", line 23, in <module>
main()
File "/home/runner/work/python-advanced/python-advanced/notebooks/fact_sysargv.py", line 14, in main
number = int(sys.argv[1])
IndexError: list index out of range
# et avec deux paramètres, le second est simplement ignoré...
! python fact_sysargv.py 6 20
argv=['fact_sysargv.py', '6', '20']
factorial(6)=720
les options¶
en plus de tout cela, il arrive fréquemment qu’on ait envie de passer des options; par exemple, notre programme pourrait avoir deux modes d’affichage, bavard ou pas.
traditionnellement, les options commencent par un tiret haut, comme ceci :
# le -v est une option (v pour verbose)
# du coup le programme fonctionne en mode bavard
! python fact_sysargv2.py -v 6
factorial(6)=720
# sans option il marche en mode silencieux, pas de baratin
! python fact_sysargv2.py 6
720
Vous pouvez regarder le code de fact_sysargv2.py
pour voir comment on a implémenté cela; notez surtout que le code devient vite un peu torturé, même pour traiter ce cas hyper-simple où on n’a qu’une seule option
argparse
¶
argparse
est un module de la librairie standard, conçu spécialement pour traiter de manière plus lisible ce genre de cas
on ne va pas ici entrer dans les détails, mais simplement montrer le code qui se comporterait comme fact_sysargv2
.py
%cat fact_argparse.py
import sys
from argparse import ArgumentParser
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
def main():
# on construit un objet ArgumentParser
parser = ArgumentParser()
# ensuite le code est déclaratif
# on se contente de dire quel.le.s sont les options et paramètres
parser.add_argument("-v", "--verbose", default=False, action='store_true',
# en donnant un petit text d'explication
help="enable verbose mode")
# on peut aussi dire leur type, pas besoin de les convertir
parser.add_argument("number", type=int, help="the input integer")
# on déclenche l'analyse de la ligne de commande
# en fonction de ces spécifications
args = parser.parse_args()
# et on récupère les valeurs qui nous intéressent
# dans les attributs du résultat de parse_args()
verbose = args.verbose
number = args.number
# le reste est comme avant
if verbose:
print(f"factorial({number})={factorial(number)}")
else:
print(factorial(number))
# un idiome fréquent : pour les points d'entrée
# on n'exécute ceci
# **SEULEMENT** si le fichier est vraiment le point d'entrée,
# et **PAS** s'il est importé
if __name__ == '__main__':
main()
ce code peut sembler un peu plus bavard que tout à l’heure; mais d’abord il donne un programme plus agréable à utiliser
# si on ne passe pas les paramètres imposés,
# le programme ne fait rien,
# mais il nous explique comment on aurait dû l'appeler
! python fact_argparse.py
usage: fact_argparse.py [-h] [-v] number
fact_argparse.py: error: the following arguments are required: number
# on peut avoir de l'aide
! python fact_argparse.py --help
usage: fact_argparse.py [-h] [-v] number
positional arguments:
number the input integer
optional arguments:
-h, --help show this help message and exit
-v, --verbose enable verbose mode
# et les options sont disponibles en version courte
# (-v comme tout à l'heure)
# mais aussi en version longue
! python fact_argparse.py --verbose 6
factorial(6)=720
mais fondamentalement, le plus gros avantage c’est la lisibilité, et la simplicité de maintenance, car analyser les options “à la main” devient rapidement compliqué, fouillis, et donc peu lisible et peu maintenable.
pour tous les détails sur argparse
, reportez-vous à ce tutoriel
packaging¶
on va conclure ce notebook sur une note plus avancée
si maintenant vous avez besoin que votre lanceur s’installe en même temps que le reste de votre librairie, vous allez utilisez setup.py
et écrire une clause comme ceci :
setup(
...,
entry_points={
'console_scripts': [
'lanceur = monpackage.fact_argparse:main',
]
)
et de cette façon, après avoir installé le package avec pip install
(localement ou pas), vous ou votre utilisateur pourra lancer la commande bidule
qui sera branchée sur la fonction main
du module monmodule
dans le package monpackage
pour davantage de détails, reportez-vous à la documentation de setuptools
à ce sujet
conclusion¶
on vient de voir l’essentiel des techniques qui permettent d’écrire un lanceur en Python
comme vous l’avez compris on recommande d’utiliser argparse
, de préférence à l’accès direct à sys.argv
, qui donne un code plus lisible, plus maintenable, et beaucoup plus court dans la plupart des cas réels
enfin pour installer de tels lanceurs, il est préférable de sous-traiter le travail à setuptools
, qui se chargera de tous les détails - souvent sordides - pour une installation propre sur tous les OS de vos éventuels utilisateurs