[Actualité] [Python] Gérer plus facilement les arguments passés à un script
par
, 05/03/2017 à 14h43 (6178 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 lentier 0. argvs = list(sys.argv) path = argvs[0] for i in argvs[1:]: if i[0]=="-": #indicateur que cest un drapeau et non une valeur try: arguments[i] except: arguments[i] = [] cle = i else: try: cle # si une clé nest pas encore définie, on ajoute cela dans un drapeau 0, qui nest jamais quun 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 sil ny a quun drapeau passé en argument (afin quil 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.