Concepts Python avancés

Introduction à *args et **kwargs

GALODE Alexandre

Quel que soit le langage utilisé, il existe toujours des subtilités ou des concepts forts utiles et puissants qui facilitent le travail du programmeur.

Concernant Python, nous pourrons citer le packing et l'unpacking, qui sont des techniques que tout programmeur Python utilise implicitement.

C'est l'utilisation explicite de ces techniques, via l'opérateur splat (*), que je vous propose de découvrir maintenant.

Commentez

- Titre : Concepts Python avancés

- Auteur : GALODE Alexandre

- Parution : 20 septembre 2015

- Public visé :

- Licence : Le contenu de cet article est rédigé par deusyss et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Introduction

- II. Principe

- III. *args

- IV. **kwargs

- V. Utilisation conjointe

- VI. Quand les employer

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Nous allons voir ici des concepts Python forts pratiques et puissants. Le packing et l'unpacking, et par extension via l'opérateur « * » (splat), *args et **kwargs, font partie de ces petits plus Python nous simplifiant grandement la vie.

II - Principe

L'opérateur splat, l'étoile, possède plusieurs rôles en Python.

De base, vous le connaissez pour réaliser une multiplication, une puissance, ou une duplication.

1. # Multiplication

2. print(2 * 3)

4. # Puissance

5. print(2**3)

7. # Duplication

8. ma_liste=[1,2,3]

9. print(3 * ma_liste)

Cependant, il ne se limite pas à cela. L'utiliser explicitement devant une variable, en simple (*) ou double exemplaire (**) permet de manipuler des notions Python plus évoluées.

Le packing et l'unpacking sont deux concepts Python complémentaires qui offrent la possibilité de transformer une succession d'arguments, nommés ou non, en liste/tuple ou dictionnaire et vice-versa.

Vous avez sûrement dû déjà l'utiliser implicitement. Voici un petit exemple d'illustration :

1. # Unpacking: fonctionne avec des tuples, listes, strings, et tout autre iterable

2. # le tuple (2,3) va etre depaquete et sauvegarde dans les variables a et b

3. a, b = (2,3)

4. print a

5. print b

7. # Packing

8. # Les valeurs de a et b sont empaquetees dans une liste l

9. l = [a, b]

10. print l

Nous avons utilisé ici un modèle nommé « tuple unpacking » et « tuple packing ». Il s'agit d'unpacking et de packing, que nous pourrions définir comme implicites, dans la mesure où l'utilisation de l'opérateur splat est inutile.

À l'inverse, nous réaliserons de l'unpacking explicite, et du packing explicite, via l'opérateur splat.

1. # Unpacking explicite avec une liste, puis un dictionnaire

2. def aire_rectangle(a, b):

3. return a*b

5. def aire_rectangle(cote1=0, cote2=0):

6. return cote1*cote2

8. if __name__ == '__main__':

9. rec1 = [3, 8]

10. rec2 = {'cote1':4, 'cote2':8}

11. # La liste rec1 va etre depaquetee en arguments unitaires

12. print aire_rectangle(*rec1)

13. # Le dictionnaire rec2 va etre depaquete en arguments unitaires

14. print aire_rectangle(**rec2)

1. # packing explicite

2. def aire_rectangle(*args):  # les arguments passes en parametre sont paquetes dans args qui se comporte comme un tuple

3. if len(args) == 2:

4. return args[0]*args[1]

5. else:

6. print('Merci de stipuler deux parametres')

8. def aire_rectangle2(**kwargs):  # les arguments passes en parametre sont paquetes dans kwargs qui se comporte comme un dictionnaire

9. if len(kwargs) == 2:

10. result = 1

11. for key, value in kwargs.items():

12. result *=value

13. return result

14. else:

15. print('Merci de stipuler deux parametres')

17. if __name__ == '__main__':

18. # Une liste va etre creee a partir des arguments fournis

19. print aire_rectangle(3,8)

20. # Un dictionnaire va etre cree a partir des arguments nommes

21. print aire_rectangle2(cote1=4, cote2=8)

Reposant sur ces principes, *args et **kwargs permettent, dans la définition d'une fonction de récupérer les arguments, successivement sous forme de tuple et de dictionnaire, et donc de réaliser du packing. À l'inverse, comme argument dans l'appel d'une fonction, *args et **kwargs revient à effectuer de l'unpacking.

opérateur (*)

opérateur (**)

Comme paramètre dans la définition d'une fonction

def aire_rectangle(*args):
...

Les arguments non nommés passés en paramètre sont empaquetés (packing) dans un tuple.

def aire_rectangle(**args):
...

Les arguments nommés passés en paramètre sont empaquetés (packing) dans un dictionnaire.

Comme argument dans l'appel d'une fonction

def aire_rectangle (a,b):
...

rec=[8, 3]
print aire_rectangle(*rec)

La liste rec est dépaquetée (unpacking) en une suite d'arguments non nommés.

def aire_rectangle(cote1=0,cote2=0):
...

rec={'cote1':8, 'cote2':3}
print aire_rectangle(**rec)

Le dictionnaire rec est dépaqueté (unpacking) en une suite d'arguments nommés.

III - *args

Voyons comment on pourrait réaliser un traitement qui consiste à remplacer une chaîne de caractères par une autre dans une succession de fichiers texte.

Une première solution pourrait consister à boucler sur l'ensemble des fichiers avec une boucle for :

1. # Avec une boucle FOR

2. def traitement(a_remplacer, remplacant, fichier):

3. content = ""

4. with open(fichier, 'r') as fichier_only:

5. for line in fichier_only.readlines():

6. line = line.replace(a_remplacer, remplacant)

7. content += line

9. with open(fichier, 'w') as fichier_only:

10. fichier_only.write(content)

12. if __name__ == '__main__':

13. # parcours des fichiers dans une boucle for

14. for fichier in ('fichier1.txt', 'fichier2.txt', 'fichiern.txt'):

15. traitement('toto', 'tata', fichier)

En deuxième solution, on pourrait passer une liste de fichiers en arguments de la fonction. Le parcours des fichiers de la liste s'effectue avec une boucle for dans la fonction de traitement :

1. # Avec une liste

2. def traitement(a_remplacer, remplacant, liste):

3. for idx, fichier in enumerate(liste):

4. content = ""

5. with open(fichier, 'r') as fichier_only:

6. for line in fichier_only.readlines():

7. line = line.replace(a_remplacer, remplacant)

8. content += line

10. with open(fichier, 'w') as fichier_only:

11. fichier_only.write(content)

13. if __name__ == '__main__':

14. ma_liste=['fichier1.txt', 'fichier2.txt', 'fichiern.txt']

15. traitement('toto', 'tata', ma_liste)

Si tout cela fonctionne, il n'en reste pas moins que le code n'est pas très optimisé. La troisième solution est donc l'utilisation de *args dans la définition de la fonction de traitement.

*args va récupérer l'ensemble des arguments non nommés, transmis lors de l'appel de la fonction, afin de les assembler sous forme de liste (packing).

À l'intérieur de la fonction, args deviendra une liste comme une autre que vous pourrez exploiter via une boucle for par exemple.

1. # Avec *args

2. def traitement(a_remplacer, remplacant, *args):

3. for idx, fichier in enumerate(args):

4. content = ""

5. with open(fichier, 'r') as fichier_only:

6. for line in fichier_only.readlines():

7. line = line.replace(a_remplacer, remplacant)

8. content += line

10. with open(fichier, 'w') as fichier_only:

11. fichier_only.write(content)

13. if __name__ == '__main__':

14. traitement('riri', 'fifi', 'fichier1.txt')

15. traitement('toto', 'tata', 'fichier1.txt', 'fichier2.txt', 'fichiern.txt')  # le nombre d'arguments est variable

16. liste_fichiers=['fichier3.txt', 'fichier4.txt', 'fichiers5.txt']

17. traitement('fifi', 'loulou', *liste_fichiers) # mais on peut aussi passer une liste,  sans traitement, grace a l'operateur (*)

Pour information, nous ne sommes pas obligés d'utiliser obligatoirement le terme args. Il s'agit plus d'une convention de nommage, qu'il est recommandé de respecter.

Vous obtenez ainsi une fonction de type dynamique, sans avoir à effectuer de prétraitement avant l'appel de la fonction. Notez que le code de la fonction traitement, lui, évolue peu par rapport à la solution précédente.

Un autre exemple de packing et unpacking :

1. def section_rectangulaire(a, b):

2. return a * b

4. def volume_parallelepipede(h, *args_section_rect):

5. return h * section_rectangulaire(*args_section_rect)

7. print (section_rectangulaire(5, 3)) # resultat = 15

9. print (volume_parallelepipede(4, 3, 8)) # resultat = 96

La fonction volume_parallelepipede reprend les deux derniers arguments pour calculer la surface avec la fonction section_rectangulaire.

IV - **kwargs

Tout comme pour *args, **kwargs constitue l'option la plus intéressante, lorsqu'il s'agit de transmettre un nombre variable d'arguments.

Cependant, ces arguments doivent être nommés. Lors d'un appel à la fonction, **kwargs va alors prendre l'ensemble des arguments nommés, sans avoir besoin de les déclarer dans le prototype de la fonction, afin de les empaqueter dans un dictionnaire.

Le nom de chaque argument deviendra alors une clé, et la valeur sera copiée dans le dictionnaire.

Une fois dans la fonction, nous n'aurons alors plus qu'à utiliser kwargs comme un simple dictionnaire.

1. def presentation(title, **kwargs):

2. print ('---- %s ----' %(title))

4. name=kwargs.get('name', None)

5. firstname=kwargs.get('firstname', None)

7. if (name and firstname):

8. print('mon nom est %s, %s %s' %(name, firstname, name))

9. elif (firstname and not(name)):

10. print('Mon prenom est %s' %(firstname))

11. elif (not(firstname) and name):

12. print('Mon nom est %s' %(name))

13. else:

14. print('Je ne suis personne...')

16. if __name__ == "__main__":

17. presentation('Prenom+nom', firstname='James', name='Bond')

18. presentation('Seulement un prenom', firstname='James')

19. presentation('Seulement un nom', name='Bond')

20. presentation('ni nom, ni prenom')

Ce qui vous donnera :

Pour information, là aussi nous ne sommes pas obligés d'utiliser obligatoirement le terme kwargs. Il s'agit plus d'une convention de nommage, qu'il est recommandé de respecter.

V - Utilisation conjointe

L'utilisation de *args et **kwargs n'est pas exclusive. Entendez par là que vous pouvez parfaitement les utiliser conjointement afin de disposer dans votre fonction à la fois d'une liste et d'un dictionnaire d'arguments.

1. def print_my_list(title_list, title_dict, *my_list, **my_dict):

2. # Dans ce namespace, my_dict est un dictionnaire et my_list une liste

4. print('ici les elements de la liste %s' % (title_list))

5. for idx,value in enumerate(my_list):

6. print('Index %02d: %s' % (idx, value))

8. print('voici les elements du dictionnaire %s' % (title_dict))

9. for key,value in my_dict.items():

10. print('Element %s: %s' % (key, value))

12. if __name__ == '__main__':

13. print_my_list('liste 0', 'dict 0', 0, 1, 2, 3, zero=0, un=1, deux=2, trois=3, quatre=4, cinq=5)

Comme on peut le voir dans cet exemple, il est indispensable de respecter l'ordre du prototype de la fonction. Dans notre cas, tous les paramètres non nommés, avant les paramètres nommés. Je vous invite à les inverser dans l'appel afin de constater par vous-même le résultat.

VI - Quand les employer

Il s'agit là d'une question complémentaire à une autre qui a pu vous venir à l'esprit : « Pourquoi utiliser *args et **kwargs ? ». Ou du moins, pourquoi les utiliser dans les définitions de fonctions.

En tant que paramètres dans la définition de fonction, *args et **kwargs permettent de récupérer les arguments passés, empaquetés dans un tuple ou un dictionnaire. Dans le corps de la fonction, vous pouvez alors manipuler les arguments transmis à votre guise, comme avec n'importe quel tuple ou dictionnaire. Vous pouvez alors tenir compte du nombre d'arguments transmis, non connu à l'avance ; vérifier la présence de tel argument nommé. Les arguments empaquetés peuvent être repris tels quels comme paramètre lors de l'appel d'une fonction fille.

Dans les appels de fonction, le nombre d'arguments transmis est variable ou provenant d'une liste ou un dictionnaire dépaqueté.

Tout ceci rend vos fonctions et vos appels de fonction plus dynamiques, plus souples avec du code souvent plus concis.

VII - Conclusion

Comme nous venons de le voir à travers cet article, le packing et l'unpacking sont très utiles et puissants une fois leur principe assimilé.

Les possibilités offertes par *args et **kwargs, opérateurs qui en découlent, sont également extrêmement importantes et permettent d'améliorer notre code, et de le rendre encore plus dynamique.

J'espère que cette petite présentation rapide vous aura appris de nouvelles choses et vous permettra à l'avenir de rendre votre code plus efficient.

VIII - Remerciements

- f-leb

- ClaudeLELOUP