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.
2.
3.
4.
5.
6.
7.
8.
9.
# Multiplication
print
(
2
*
3
)
# Puissance
print
(
2
**
3
)
# Duplication
ma_liste=
[1
,2
,3
]
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
# Unpacking: fonctionne avec des tuples, listes, strings, et tout autre iterable
# le tuple (2,3) va etre depaquete et sauvegarde dans les variables a et b
a, b =
(
2
,3
)
print
a
print
b
# Packing
# Les valeurs de a et b sont empaquetees dans une liste l
l =
[a, b]
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
# Unpacking explicite avec une liste, puis un dictionnaire
def
aire_rectangle
(
a, b):
return
a*
b
def
aire_rectangle
(
cote1=
0
, cote2=
0
):
return
cote1*
cote2
if
__name__
==
'__main__'
:
rec1 =
[3
, 8
]
rec2 =
{'cote1'
:4
, 'cote2'
:8
}
# La liste rec1 va etre depaquetee en arguments unitaires
print
aire_rectangle
(*
rec1)
# Le dictionnaire rec2 va etre depaquete en arguments unitaires
print
aire_rectangle
(**
rec2)
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
# packing explicite
def
aire_rectangle
(*
args): # les arguments passes en parametre sont paquetes dans args qui se comporte comme un tuple
if
len(
args) ==
2
:
return
args[0
]*
args[1
]
else
:
print
(
'Merci de stipuler deux parametres'
)
def
aire_rectangle2
(**
kwargs): # les arguments passes en parametre sont paquetes dans kwargs qui se comporte comme un dictionnaire
if
len(
kwargs) ==
2
:
result =
1
for
key, value in
kwargs.items
(
):
result *=
value
return
result
else
:
print
(
'Merci de stipuler deux parametres'
)
if
__name__
==
'__main__'
:
# Une liste va etre creee a partir des arguments fournis
print
aire_rectangle
(
3
,8
)
# Un dictionnaire va etre cree a partir des arguments nommes
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 |
Sélectionnez
|
Sélectionnez
|
Comme argument dans l'appel d'une fonction |
Sélectionnez
|
Sélectionnez
|
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
:
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
# Avec une boucle FOR
def
traitement
(
a_remplacer, remplacant, fichier):
content =
""
with
open(
fichier, 'r'
) as
fichier_only:
for
line in
fichier_only.readlines
(
):
line =
line.replace
(
a_remplacer, remplacant)
content +=
line
with
open(
fichier, 'w'
) as
fichier_only:
fichier_only.write
(
content)
if
__name__
==
'__main__'
:
# parcours des fichiers dans une boucle for
for
fichier in
(
'fichier1.txt'
, 'fichier2.txt'
, 'fichiern.txt'
):
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
# Avec une liste
def
traitement
(
a_remplacer, remplacant, liste):
for
idx, fichier in
enumerate(
liste):
content =
""
with
open(
fichier, 'r'
) as
fichier_only:
for
line in
fichier_only.readlines
(
):
line =
line.replace
(
a_remplacer, remplacant)
content +=
line
with
open(
fichier, 'w'
) as
fichier_only:
fichier_only.write
(
content)
if
__name__
==
'__main__'
:
ma_liste=
['fichier1.txt'
, 'fichier2.txt'
, 'fichiern.txt'
]
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
# Avec *args
def
traitement
(
a_remplacer, remplacant, *
args):
for
idx, fichier in
enumerate(
args):
content =
""
with
open(
fichier, 'r'
) as
fichier_only:
for
line in
fichier_only.readlines
(
):
line =
line.replace
(
a_remplacer, remplacant)
content +=
line
with
open(
fichier, 'w'
) as
fichier_only:
fichier_only.write
(
content)
if
__name__
==
'__main__'
:
traitement
(
'riri'
, 'fifi'
, 'fichier1.txt'
)
traitement
(
'toto'
, 'tata'
, 'fichier1.txt'
, 'fichier2.txt'
, 'fichiern.txt'
) # le nombre d'arguments est variable
liste_fichiers=
['fichier3.txt'
, 'fichier4.txt'
, 'fichiers5.txt'
]
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 :
2.
3.
4.
5.
6.
7.
8.
9.
def
section_rectangulaire
(
a, b):
return
a *
b
def
volume_parallelepipede
(
h, *
args_section_rect):
return
h *
section_rectangulaire
(*
args_section_rect)
print
(
section_rectangulaire
(
5
, 3
)) # resultat = 15
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
def
presentation
(
title, **
kwargs):
print
(
'----
%s
----'
%(
title))
name=
kwargs.get
(
'name'
, None
)
firstname=
kwargs.get
(
'firstname'
, None
)
if
(
name and
firstname):
print
(
'mon nom est
%s
,
%s
%s
'
%(
name, firstname, name))
elif
(
firstname and
not
(
name)):
print
(
'Mon prenom est
%s
'
%(
firstname))
elif
(
not
(
firstname) and
name):
print
(
'Mon nom est
%s
'
%(
name))
else
:
print
(
'Je ne suis personne...'
)
if
__name__
==
"__main__"
:
presentation
(
'Prenom+nom'
, firstname=
'James'
, name=
'Bond'
)
presentation
(
'Seulement un prenom'
, firstname=
'James'
)
presentation
(
'Seulement un nom'
, name=
'Bond'
)
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
def
print_my_list
(
title_list, title_dict, *
my_list, **
my_dict):
# Dans ce namespace, my_dict est un dictionnaire et my_list une liste
print
(
'ici les elements de la liste
%s
'
%
(
title_list))
for
idx,value in
enumerate(
my_list):
print
(
'Index
%02d
:
%s
'
%
(
idx, value))
print
(
'voici les elements du dictionnaire
%s
'
%
(
title_dict))
for
key,value in
my_dict.items
(
):
print
(
'Element
%s
:
%s
'
%
(
key, value))
if
__name__
==
'__main__'
:
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.