Python, la tête dans les « * »
L’utilisation du signe étoile (*) en Python est très simple, mais certains cas sont peu intuitifs. Les nouveaux venus ont souvent besoin d’un peu plus d’explications que ce que donne la doc. Les utilisateurs d’autres langages sont généralement déroutés car ils sont habitués certaines fonctionnalités qu’on ne retrouvent pas en Python.
Ce que « * » ne permet pas de faire
Il n’y a pas de pointeurs en Python, et les passages par références sont automatiques. Du coup :
mon_objet = MaClasse() mon_pointeur = *mon_objet ma_valeur = **mon_pointeur
N’existe pas en Python. On ne peut pas récupérer un pointeur. On ne peut pas choisir si l’on passe une variable par valeur ou par référence. Tout est automatique et transparent.
Les usages basiques de « * »
La multiplication et la puissance fonctionnent comme on l’attend :
>>> print(2*3) # multiplier 2 par 3 6 >>> print(2**3) # élever 2 à la puissance 3 8
Mais déjà, Python se démarque du lot car l’opérateur * est surchargé par défaut, et peut s’appliquer aux chaînes de caractères et aux listes. Pour les chaîne, c’est simple :
>>> print("a" * 3) # on peut multiplier une chaîne par un nombre, et cela donne une chaîne aaa >>> print("a" ** 3) # ça ne marche pas avec les puissances TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
Pour les listes, c’est plus subtile. Une liste de nombre se multiplie sans y penser :
>>> l = [0, 1, 3] >>> print(l * 2) # on peut multiplier une liste par un nombre, cela donne une liste [0, 1, 3, 0, 1, 3] >>> print(l ** 2) # ça ne marche pas avec les puissances TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'int'
En revanche, multiplier une liste d’objets modifiables ne fait que répéter la référence vers cet objet:
>>> l = [{}] # on fait une liste contenant un dictionnaire >>> dicos = l * 3 # on peut multiplier une liste par un nombre, cela donne une liste >>> print(dicos) [{}, {}, {}]
On a l’impression que le comportement est le même que précédemment, en fait pas du tout. Ici on a pas une liste de 3 dictionnaires, mais une liste de 3 références faire le même dictionnaire. Si on modifie le premier élément de la liste, tous sont modifiés :
>>> d = dicos[0] # on récupère ce qu'on croit être le premier dictionnaire >>> d["Nouvelle cle"] = "Nouvelle valeur" # on le modifie >>> print(dicos) # afficher la liste montre que les 3 dictionnaires sont en fait un seul et même objet [{'Nouvelle cle': 'Nouvelle valeur'}, {'Nouvelle cle': 'Nouvelle valeur'}, {'Nouvelle cle': 'Nouvelle valeur'}]
Unpacking
Python intègre une fonctionnalité, l’unpacking, qui permet de prendre chaque élément d’une séquence et de les attribuer à des variables distinctes, d’un seul coup. C’est un raccourcis très partique :
>>> drapeau = ("bleu", "blanc", "rouge") # ici on utilise un tuple, mais ça marche avec les listes >>> premiere_couleur = drapeau[0] >>> deuxieme_couleur = drapeau[1] >>> troisieme_couleur = drapeau[2] >>> print(premiere_couleur) 'bleu' >>> print(deuxieme_couleur) 'blanc' >>> print(troisieme_couleur) 'rouge' >>> couleur1, couleur2, couleur3 = drapeau # la même opération, en une ligne grace à l'unpacking >>> print(couleur1) 'bleu' >>> print(couleur2) 'blanc' >>> print(couleur3) 'rouge'
Vous n’avez rien à faire, l’unpacking est automatique : il suffit de mettre à gauche du signe « = » le même nombre de variables qu’il y a d’élément dans la séquence à droite du signe « = ». Dans le cas contraire, Python râle :
>>> un, deux = drapeau ValueError: too many values to unpack >>> un, deux, trois, quatre = drapeau ValueError: need more than 3 values to unpack
Quel rapport avec « * » ? Et bien il permet de forcer l’unpacking dans le cas où c’est ambigüe. Faisons une petite fonction de test qui ne fait qu’afficher chacun de ses paramètres :
>>> def afficher_trois_elements(elem1, elem2=None, elem3=None): ... print(elem1) ... print(elem2) ... print(elem3) ... ... >>> afficher_trois_elements(drapeau) ('bleu', 'blanc', 'rouge') None None
Passer drapeau affiche logiquement le tuple comme premier paramètre, et ensuite les valeurs par défaut du premier et du second paramètre. En utilisant « * », nous pouvons forcer l’unpacking de telle sorte que les valeurs du tuple soient passées individuellement comme autant de paramètres :
>>> afficher_trois_elements(*drapeau) bleu blanc rouge
Très pratique quand vous utiliser une collection tout au long du programme pour vous éviter de sans cesse trainer des variables intermédiaires. D’autant que ça marche combiné aux slices :
>>> l = [1, 2, 3, "element que l'on ne veut pas"] >>> afficher_trois_elements(*l[:-1]) 1 2 3
Encore mieux, on peut utiliser « ** » pour forcer l’unpacking des dictionnaires. Les valeurs du dictionnaires deviennent les valeurs des paramètres, mais cette association se fait par nom : chaque clé du dictionnaire doit correspondre à un nom de paramètre. Ainsi :
>>> elements = {"elem1" : "eau", "elem2" : "feu", "elem3" : "air"} # les clés ont le bon nom >>> afficher_trois_elements(**elements) eau feu air
Si une clé ne possède pas le nom adéquat, tout plante :
>>> elements = {"elem1" : "eau", "elem2" : "feu", "rien_a_voir" : "air"} >>> afficher_trois_elements(**elements) TypeError: afficher_trois_elements() got an unexpected keyword argument 'rien_a_voir'
Une autre erreur courante est d’utiliser « * » avec un dictionnaire. Dans ce cas l’unpacking fonctionne, mais comme itérer sur une dictionnaire donne une liste de clés, c’est comme si vous passiez une liste en paramètres contenant les clés :
>>> elements = {"elem1" : "eau", "elem2" : "feu", "elem3" : "air"} >>> afficher_trois_elements(*elements) elem2 elem3 elem1
Si vous donnez moins de valeurs qu’il n’y a de paramètres, Python remplit tout ce qu’il peut :
>>> afficher_trois_elements(*drapeau[:-1]) bleu blanc None >>> elements = {"elem1" : "eau"} >>> afficher_trois_elements(**elements) eau None None
Dans le cas inverse – si i il y a plus d’élements que de paramètres – Python refuse les séquences, mais fait au mieux avec les dictionnaires :
>>> forces = ("rouge", "bleu", "jaune", "rose", "vert") >>> afficher_trois_elements(*forces) TypeError: afficher_trois_elements() takes at most 3 arguments (5 given) >>> elements = {"elem1" : "eau", "elem2" : "feu", "elem3" : "air", "elem3" : "terre"} >>> afficher_trois_elements(**elements) eau feu terre
Paramétrage dynamique
Il est parfois pratique de définir une fonction qui accepte un nombre infini d’arguments. Par exemple, on a une fonction qui multiplie ses arguments entre eux :
>>> def multiply(a, b): ... return a * b # attention, là on utilise « * » pour multiplier, ne cherchez rien de compliqué ;-) ... >>> print(multiply(2, 3)) 6
Bien sûr, si on veut rajouter un troisième paramètre, il faut la réécrire. Pareil pour un quatrième. Finalement, on finit par demander de passer une liste pour permettre un nombre arbitraire :
>>> def multiply(elements_a_multiplier): ... res = 1 ... for i in elements_a_multiplier: ... res = res * i ... return res ... >>> multiply((1, 2, 3, 4)) 24
Et bien sachez qu’il existe une autre possibilité, autoriser l’ajout d’une infinité de paramètres ! Cela se fait bien sur avec « * ».
>>> def multiply(*tous_les_elements): # on ne change pas grand chose, on rajoute juste « * » ... res = 1 ... for i in tous_les_elements): ... res = res * i ... return res ... >>> multiply(1, 2, 3) # mais plus besoin d'une séquence ! 26 >>> multiply(1, 2, 3, 4, 5) 120
Comment ça marche ? C’est simple, tout les arguments sont stockés dans une liste, et cette liste est le paramètre que l’on a désigné par « * ». Ce système très puissant peut être utilisé conjointement avec des paramètres normaux :
>>>def afficher(elem1, elem2, *elemx): ... print(elem1) ... print(elem2) ... for e in elemx : ... print("(*) %s" % e) … >>> afficher("Toi", "Moi", "Luke", "Anakin", "Obi Wan", "Robert") Toi Moi (*) Luke (*) Anakin (*) Obi Wan (*) Robert
La seule condition est de mettre « * » sur un paramètre situé après tous les autres. « * » est toujours en dernier, et il n’apparait qu’une seule fois. Enfin, il existe une convention pour le nom de cet argument : « *args ».
Bonne nouvelle, on peut utiliser aussi « ** ». Comme on peut s’y attendre, il permet de récupérer aussi une infinité de paramètres, mais sous forme de dictionnaire. Cela signifie qu’il ne récupère que les paramètres nommés :
>>> def afficher_recette(recette, **ingredients): # ingrédients sera un dictionnaire ... print(recette) ... for ingredient in ingredients.iteritems(): ... print " - %s : %s" % ingredient ... >>> afficher_recette("moukraines à la glaviouse", ... creme="trop", # on doit donner le nom de ce paramètre ... moukraines= "suffisamment", ... glaviouse="si disponible") # mais l'ordre des paramètres importe peu moukraines à la glaviouse - glaviouse : si disponible - creme : trop - moukraines : suffisamment
Il faut également mettre « ** » après tous les autres arguments. La convention pour nommer ce paramètre est « **kwargs », pour « keyword arguments ».
Enfin, on peut mélanger tout ça d’un coup :
>>> def affichage_hybride(parametre_normal, ... parametre_avec_default="valeur par défaut", ... *args, ... **kwargs): ... print(parametre_normal) ... print(parametre_avec_default) ... print(args) ... print(kwargs) ... >>> affichage_hybride("param1", "param2", "infini1", "infini2", kwinfini1=1, kwinfini2=2) param1 param2 ('infini1', 'infini2') {'kwinfini1': 1, 'kwinfini2': 2}
On doit absolument mettre les paramètres dans cet ordre :
1. paramètres normaux et obligatoires;
2. paramètres normaux facultatifs (valeur par défaut);
3. paramètres dynamiques;
4. paramètres dynamiques nommés.
En plus, cela permet en effet de faire jouer les valeurs par défaut de manière très souple :
>>> affichage_hybride("param tout seul") param tout seul valeur par défaut () {}
Si vous vous sentez à l’aise avec tout ça, vous pouvez mélanger plusieurs usages de « * » d’un coup. Je vous laisse donc en guise de conclusion un petit combo qui utilise un code précédent:
>>> def multiply(*args): ... res = 1 ... for i in elements_a_multiplier: ... res = res * i ... return res ... >>> print(multiply(*([2]*6)) == 2**6) True
22/02/2010 à 22:24
>>> def multiply(*tous_les_elements): # on ne change pas grand chose, on rajoute juste « * »
… res = 1
… for i in elements_a_multiplier:
… res = res * i
… return res
…
>>> multiply(1, 2, 3)
# mais plus besoin d’une séquence !
26
>>> multiply(1, 2, 3, 4, 5)
120
Une petite coquille à la première ligne “elements_a_multiplier” à la place de “tous_les_elements”.
Je poste rarement mais j’apprécie beaucoup ton blog, que j’ai découvert pour android et que je suis pour python et android (un nouveau post pour python à venir?), continue…
22/02/2010 à 22:24
remplacer “pour python” par “pour android” à la dernière ligne
23/02/2010 à 1:46
Merci 2beta, à la fois pour la sympathie et la coquille trouvée.
01/07/2010 à 19:06
Juste un merci pour toutes ces info sur ” * “.