Voir le flux RSS

nothus

[Actualité] [Python] Gérer plus facilement les arguments passés à un script

Noter ce billet
par , 05/03/2017 à 13h43 (758 Affichages)
A la fin de ce petit article, je proposerai une ébauche de module téléchargeable depuis GitHub. Le code indiqué ici est valide pour 3.6, sans garantie pour les versions antérieures - même si tout devrait globalement fonctionner pour Python 3.

Sous Python, l'accès aux arguments (sans passer par les circonvolutions des modules officiels de Python), se fait très facilement :
Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
import sys
print(sys.argv)

Qui affichera... pas grand chose dans l'IDLE de Python, sauf un laconique [''] - ce qui nous indique déjà que son retour est une liste !

Il existe d'autres manières, plus complètes mais souvent plus délicates à mettre en œuvre.

Or le plus régulièrement, particulièrement lorsque le script n'a pas une destination particulière à augmenter en complexité ou en partage (ou par flemme, ou par envie de s'embêter plus tard...), il est plus simple d'utiliser sys.argv même si on y trouve rapidement des limites. L'ennemi du bien étant le mieux, il est aussi tout à fait possible de ne jamais s'occuper des arguments et de tout mettre dans un fichier de configuration. Pour ma part je préfère un INI ou autre bien fichu et documenté que quelques lignes.

En voici quelques raisons, sous forme d'un bref comparatif :

Avantages des arguments
  • les arguments peuvent modifier - "à la marge" - certains comportements, par exemple lorsqu'un serveur se lance (choix du port)
  • les arguments n'imposent pas la création d'un fichier pour démarrer (si problème d'écriture sur le disque ; ce qui évite en soi un problème de sécurité - éviter d'écriture n'importe quoi - mais en rajoute un autre - permettre potentiellement d'écrire n'importe quoi)
  • les arguments sont plus rapides à utiliser, pour peu que l'on connaisse sur le point des ongles toutes les ramifications de son script
  • les arguments peuvent donner plus facilement accès à la documentation (-h)


Avantages des fichiers de configuration
  • sans accès à l'écriture sur le disque, il est possible d'ajouter une protection supplémentaire pour éviter à un script de se lancer
  • l'argument est peu lisible, les erreurs plus faciles ("-r" détruit tout et "-t" fait une sauvegarde : on se trompe de touche, et c'est le drame...)
  • les fichiers de configuration n'ont pas vraiment de limites de tailles ou même permettent des choses intéressantes, comme les sections des fichiers INI
  • un fichier de configuration peut se sauvegarder, se partager, parfois plus facilement qu'une succession de drapeaux peu verbeux


Mais la bataille entre partisans des fichiers de configuration ou des arguments de script n'est pas le sujet... sys.argv indique toujours le chemin (ou son absence) et donc le nom du fichier (ou son absence) qui "porte" le script. Ainsi le même code qu'au-dessus dans "demo1.py" donnera dans mon cas ['C:/Users/Julien/Google Drive/Développement/lanceur/demo1.py'].

Les autres éléments de la liste seront les différentes briques qui forment la totalité des arguments : sans distinction entre les drapeaux (les clés d’un futur dictionnaire en quelque sorte) et les contenus associés (les valeurs de ce même futur dictionnaire). Ainsi mon "demo1.py" de tout l'heure, lancé ainsi : demo1.py -a 1 2 3 4 donnera [" ... chemin...", "-a","1","2","3","4"] !

Pire, le terminal (c-à-d interpréteurs de commandes) utilisé peut retirer certains éléments de ma commande, comme par exemple le double & qui lie deux scripts (if-then) ou utilisé de manière unique en toute fin signifiant que le script n'est plus directement rattaché à la console sous Linux. Bref ce que vous recevez par sys.argv ne doit pas être considérer comme strictement la commande de l'utilisateur (parce qu'il utilise un script bash entre ; parce qu'il lie les commandes, etc) mais comme des indications à prendre en compte.

Pour trouver ce ce qui doit nous revenir, nous allons donc prendre tout ce qu'il y a après la clé 0. Par exemple :
Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
import sys
print(sys.argv[1:])

Il suffit ensuite de parcourir cette liste raccourcie pour construire notre dictionnaire des arguments ; ce sont les tirets qui donneront l’indicateur que le drapeau est soit court (un tiret) ou long (deux tirets), et surtout n’est pas une valeur. Voici un proposition qui reprend cette construction et l’affine (notamment si une valeur est passée sans drapeau, elle s’ajoute dans le dictionnaire sous la clé "0").

Code python : 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
import sys
 
arguments = {0:[]} #attention, le drapeau "-0" renvoie une chaîne de caractère "0" et l’entier 0.  
 
argvs = list(sys.argv)
path = argvs[0] 
for i in argvs[1:]:
    if i[0]=="-": #indicateur que c’est un drapeau et non une valeur 
        try: 
            arguments[i]
        except: 
            arguments[i] = []
        cle = i 
    else:
        try:
            cle # si une clé n’est pas encore définie, on ajoute cela dans un drapeau 0, qui n’est jamais qu’un INT(0) pour la clé du dictionnaire des arguments 
        except: 
            cle = 0 
        try: 
            arguments[cle].append(i)
        except: 
            pass
try: 
    if cle not in arguments:
        arguments[cle] = [] #ce cas se présente s’il n’y a qu’un drapeau passé en argument (afin qu’il ne se perd pas) 
except: 
    pass 
 
print(arguments)

Testez demo1.py "pouet" -a "oui" -b -a "non" : vous recevrez alors en message console : {0 : ["pouet"], "a" : ["ok", "ko"], "b" : []}. Donc tout va bien ! Vous avez ainsi déjà vos arguments bien classés.

Gardez à l’esprit que j’ai priorisé un ajout et non un écrasement pour deux drapeaux identiques : demo1.py -a "non !" -a "si !" renvoie donc une liste pour le drapeau "a" qui est ["non!", "si !"] et pas seulement "si !". Ce brouillon de script peut facilement être glissé dans une classe, et y associer des opérations de transformation dès le traitement de sys.argv. Ou encore des modalités particulières d’accès comme par exemple :

Code python : 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
import sys 
import re 
 
class Configuration:
 
    argvs = list() 
    path = "" 
    arguments = {0:[]}
 
    def __init__(self):
        self.argvs = list(sys.argv)
        self.path = self.argvs[0] 
        for i in self.argvs[1:]:
            if i[0]=="-": 
                cle = i
                try: 
                    self.arguments[cle]
                except: 
                    self.arguments[cle] = []
            else:
                try:
                    cle
                except: 
                    cle = 0 
                try: 
                    self.arguments[cle].append(i)
                except: 
                    pass
        try: 
            if cle not in self.arguments:
                self.arguments[cle] = []
        except: 
            pass 
 
    def acceder(self,cle,defaut=[]):
        try:
            return self.arguments[cle]
        except: 
            return defaut 
 
if __name__=="__main__":
 
    C = Configuration()
 
    print(C.arguments) 
 
    print(C.acceder("b","Pas de b !"))
 
    print(C.acceder("a","Pas de a !"))

C’est facile et lors d’un import, c’est propre et assez généraliste pour passer dans toutes les versions de Python 3 comme je l’indiquais en introduction. Reste qu’il n’y a pas vraiment un accès totalement différencié pour chacun de mes arguments : tout sont renvoyés de la même façon. Je vais donc devoir créer une petite classe TTT ("traitement" en bon français réduit!), qui se chargera de stocker sous forme de fonctions l’accès à une variable (en somme le décorateur property un poil modifié car il prend en compte un paramètre "utilisateur" et pas seulement l’objet...).

Et puis pour ajouter une french-touch – pardon, une "touche française" :
  • on peut ajouter une méthode desinfection (la bonne traduction de sanitize en anglais), pour enlever les indicateurs de drapeaux longs ou courts qui ne nous intéressent pas ici
  • on ajoute de la documentation, du coup logiquement caler dans ma classe de traitement (au cas où l’on en change : ma classe Configuration, elle, ne change pas)


Et voici ce que devient mon "demo1.py" :

Code python : 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
83
84
85
86
87
import sys 
import re 
 
class Configuration:
 
    argvs = list() 
    path = "" 
    arguments = {0:[]}
 
    desinfecter = True 
 
    objTraitement = None 
 
    def __init__(self,desinfecter=None):
        self.desinfecter if desinfecter is True else False 
        self.argvs = list(sys.argv)
        self.path = self.argvs[0] 
        for i in self.argvs[1:]:
            if i[0]=="-": 
                cle = self.desinfection(i) if self.desinfecter is True else i 
                try: 
                    self.arguments[cle]
                except: 
                    self.arguments[cle] = []
            else:
                try:
                    cle
                except: 
                    cle = 0 
                try: 
                    self.arguments[cle].append(i)
                except: 
                    pass
        try: 
            if cle not in self.arguments:
                self.arguments[cle] = []
        except: 
            pass 
 
    def desinfection(self,i):
        i = re.sub("^[\-]+","",i) 
        return i 
 
    def acceder(self,cle,defaut=[]):
        try:
            return getattr(self.objTraitement,cle)(self.arguments) 
        except: 
            try: 
                return self.arguments[cle]
            except:
                return defaut 
 
    def documenter(self,cle):
        try:
            return getattr(self.objTraitement,cle).__doc__ 
        except:
            return False 
 
    def traitement(self,objTraitement):
        self.objTraitement = objTraitement 
 
if __name__=="__main__":
 
    class TTT:
 
        def a(self,args):
            """
                Additionner les nombres passés en arguments 
            """ 
            return sum(map(int,args["a"])) 
 
        def c(self,args):
            """
                Ceci est la documentation... de toute façon la valeur sera toujours non. 
            """ 
            return "non !" 
 
    C = Configuration()
    C.traitement(TTT()) 
 
    print(C.arguments) 
 
    print(C.acceder("b","Pas de b !"))
 
    print(C.acceder("a","Impossible de faire le total")) 
 
    print(C.documenter("a"))

Vous pouvez aussi jouer sur la classe TTT pour ne plus plus passer les arguments systématiquement et imposer à votre classe de configuration de passer par elle, pour arriver à quelque chose qui ressemble à ça :

Code python : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
def acceder(self,cle,defaut=[]):
        try:
            return getattr(self.objTraitement,cle)() 
        except: 
            return defaut


Voilà, ce petit article est terminé. Vous pourrez trouver ces deux classes dans mon GitHub : n’hésitez pas à y ajouter vos commentaires ou vos remarques de bugs.

Bon test et bon développement !

Julien.

Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Viadeo Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Twitter Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Google Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Facebook Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Digg Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Delicious Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog MySpace Envoyer le billet « [Python] Gérer plus facilement les arguments passés à un script » dans le blog Yahoo

Mis à jour 06/03/2017 à 16h10 par Nothus (Quelques coquilles d'orthographe.)

Catégories
Programmation , Python

Commentaires

  1. Avatar de dourouc05
    • |
    • permalink
    Au fait, pourquoi développer ta propre solution au lieu d'utiliser ce que la bibliothèque standard fournit (argparse avec Python 3 https://docs.python.org/3/library/argparse.html) ?
  2. Avatar de Nothus
    • |
    • permalink
    Citation Envoyé par dourouc05
    Au fait, pourquoi développer ta propre solution au lieu d'utiliser ce que la bibliothèque standard fournit (argparse avec Python 3 https://docs.python.org/3/library/argparse.html) ?
    Parce que ?

    Effectivement, beaucoup de choses sont possibles à partir des modules standards. Mon tutoriel (avec un objectif pédagogique, j'ai galéré pour apprendre seul Python) ne présente pas les choses autrement car j'utilise sys.argv qui est bien tiré d'un module standard (sys). D'ailleurs argparse passe le plus clair de sa source à utiliser les données par sys.argv et finalement nous partons tous les deux du même point, pour ne pas arriver tout à fait au même résultat (volontairement).

    Voici ce qui me bloque dans la "philosophie" de argparse : "The argparse module makes it easy to write user-friendly command-line interfaces." Le module s'oriente moins - à mon sens - sur ce qui permet de créer un contexte, que de faire le lien entre des commandes (les arguments) et des actions (les fonctions).

    L'exemple par défaut de la doc est révélateur :

    Code python : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import argparse
     
    parser = argparse.ArgumentParser(description='Process some integers.')
    parser.add_argument('integers', metavar='N', type=int, nargs='+',
                        help='an integer for the accumulator')
    parser.add_argument('--sum', dest='accumulate', action='store_const',
                        const=sum, default=max,
                        help='sum the integers (default: find the max)')
     
    args = parser.parse_args()
    print(args.accumulate(args.integers))

    Toute ma démonstration s'appuie sur une manière de concevoir soit par des fichiers de configuration, soit par des arguments, comme on interagit avec un script à son lancement (je passe sur les échanges de données après).

    Une fois que j'ai traité dans mon tutoriel les données fournies par sys.argv, j'ai le choix aussi de ne rien faire, de les utiliser (ou pas !) et d'avoir simplement un dictionnaire de données simple à utiliser, rapide et peut-être moins casse-gueule à mettre en œuvre et à débugger que argparse...

    Last but not least, je fais la distinction entre ma classe Configuration et TTT, qui seule cette dernière "s'approprie" les arguments (traitement + doc). Ainsi si j'ai plusieurs serveur lancé sur des ports différents au travers du module multiprocessing, je peux avoir plusieurs docs, plusieurs traitements différents pour un même argument donné.

    Bref mon tutoriel est orienté sur une création de contexte d'exécution.