Licence CC BY-NC-ND Thierry Parmentelat

é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