Les opérateurs étoile (* et **) en Python

En Python, chacun sait que l'opérateur * permet d'effectuer une multiplication sur des opérandes numériques, ou à répéter un opérande quelconque, tandis que l'opérateur ** permet d'effectuer une élévation à une puissance quelconque d'un opérande numérique :
print (2*3)
print (2*'Hello, world!')
print (2**3)
6
Hello, world!Hello, world!
8
Ce qui est moins connu, car elle est assez mal documentée, c'est la possibilité d'utiliser ces opérateurs pour du déballage (unpacking) d'itérables divers en vue de constituer un autre itérable. Par exemple :
x = ['A', 'B']
y = ('C', 'D')
z = [*x, *y]
print (z)
['A', 'B', 'C', 'D']
Comment est-il possible d'utiliser ces opérateurs de cette manière ? Et pour quoi faire ? L'exploration de la documentation de Python permet de constater qu'ils peuvent servir dans deux circonstances : pour créer des itérables, et pour fournir des arguments à une fonction. Explications...
Mise à jour du 19/05/2018 : Mention de notations supplémentaires pour créer un itérable à partir d'un autre.
A écouter en lisant l'article...

Des opérateurs pas très bien documentés

La documentation des opérateurs * et ** est enfouie ici, dans plusieurs sections de la documentation consacrée aux expressions, où il convient de rechercher des termes tels que "starred" et "**".
Deux grandes sections traitent de ces opérateurs :
  • ici, la section relative aux affichages (displays) : affichage de liste, de set, et de dictionnaire ;
  • , la section relative aux appels de fonction.
Dans tous ces cas, * et ** sont utilisés pour du déballage (unpacking) d'itérables.

Utiliser * et ** pour créer un itérable

Le choix du terme "affichage" (display) n'est certainement pas des plus heureux pour désigner un mécanisme qui permet de faciliter la construction d'un itérable. En effet, l'opérateur * permet de déballer un itérable (chaîne, tuple, liste, set, dictionnaire et tout objet itérable) pour en créer un autre.
Par exemple, pour donner à voir la manoeuvre sur les différents types d’itérables :
a = 'AB'
b = ('C', 'D')
c = ['E', 'F']
d = {'G', 'H'}
e = {0:'I', 1:'J'}
x = *a, *b, *c, *d, *e
print (x)
x = (*a, *b, *c, *d, *e)
print (x)
x = [*a, *b, *c, *d, *e]
print (x)
x = {*a, *b, *c, *d, *e}
print (x)
('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 0, 1)
('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 0, 1)
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 0, 1]
{0, 'G', 1, 'H', 'F', 'A', 'D', 'B', 'C', 'E'}
Pour rappel, un objet itérable est un objet dont la classe comporte des méthodes __iter__ () et __next__ (). Plutôt que de s'ennuyer à définir ces méthodes, il est plus facile de s'appuyer sur un générateur. Un tel objet peut donc être utilisé comme précédemment pour construire un autre itérable :
def f ():
	values = ['A', 'B']
	for i in range (len (values)):
		yield values [i]
a = [*f (), 'C']
print (a)
['A', 'B', 'C']
Comme il est possible de le noter dans l'exemple donné plus tôt, le déballage d'un dictionnaire retourne les clés, pas les valeurs. De plus, l'opérateur * ne permet pas de créer un dictionnaire, mais seulement un tuple, une liste ou un set.
L'opérateur ** permet de déballer un dictionnaire pour en créer un autre :
a = {0:'A', 1:'B'}
b = {'c':'C', 'd':'D'}
x = {**a, **b}
print (x)
{0: 'A', 1: 'B', 'c': 'C', 'd': 'D'}
Un itérable peut être déballé dans un itérable qui existe déjà :
a = ('B', 'C')
b = ('A', *a, 'D')
print (b)
('A', 'B', 'C', 'D')
Il faut bien noter que le déballage d'un itérable via * et ** ne vise qu'à créer un autre itérable. Pour déballer un itérable dans des variables, il faut utiliser la notation classique comme expliqué ici :
a = 'AB'
b, c = *a	# Il fallait écrire : b, c = a
SyntaxError: can't use starred expression here
Toutefois, il faut mentionner une notation intermédiaire qui permet de procéder au déballage d’un itérable pour créer un itérable contenant tous les éléments qui n’ont pas pu être déballés dans des variables :
a = ('A', 'B', 'C', 'D')
left, *middle, right = a
print (left)
print (middle)
print (right)
A
['B', 'C']
D

Utiliser * et ** pour passer des arguments à une fonction

Les opérateurs * et ** peuvent donc aussi servir dans le contexte d'un appel de fonction. Ils servent alors toujours à déballer, si ce n'est que c'est d'arguments dont il est question. C’est une utilisation singulière de ces opérateurs, car nous avons vu qu’ils ne permettent pas de déballer un itérable dans des variables au sens du déballage classique.
Par exemple, pour déballer des paramètres formels (voir ici pour en savoir plus sur les différentes catégories de paramètres) :
def f (subject, verb):
	print (subject, verb)
x = 'Amiga', 'rulez!'	# Rappel : notation simplifiée pour créer un tuple
f (*x)
x = {'verb':'rulez!', 'subject':'Amiga'}
f (**x)
Amiga rulez!
Amiga rulez!
Dans le cas d'un dictionnaire, il convient de noter les clés doivent reprendre les noms des paramètres spécifiés dans la définition de la fonction. De ce fait, il est impossible de transmettre des arguments de paramètres informels anonymes (tuple *args) à partir d'un dictionnaire, alors que cela est possible à partir d'autres itérables, comme par exemple un tuple :
def f (subject, verb, *complements):
	print (subject, verb, ' '.join (complements))
x = 'I', 'love', 'the', 'Amiga'
f (*x)
x = {'verb':'love', 'subject':'I', 'complements':('the', 'Amiga')}
f (**x)
I love the Amiga
NameError: name 'complements' is not defined
A l'inverse, il est impossible de transmettre un dictionnaire à partir d'un itérable qui n'est pas un dictionnaire.
La forme la plus complexe d’appel s’appuyant sur le déballage implique donc deux itérables, un premier qui n'est pas un dictionnaire pour fournir les arguments des paramètres formels (nécessairement anonymes si des paramètres informels nommés sont présents, comme expliqué ici) et informels anonymes, et un dictionnaire pour fournir les arguments des paramètres informels nommés (au passage, noter l'usage de * et ** sur des littéraux) :
def f (subject, *args, **kwargs):
	print (subject, ' '.join (args), kwargs['computer'], kwargs['model'])
f (*('I', 'love', 'the'), **{'computer':'Amiga', 'model':'500'})
I love the Amiga 500
Dans ces conditions, on comprend que s'il faut relayer le tuple et/ou le dictionnaire d'une fonction à une autre toujours via des paramètres informels, cela devra se faire en les déballant au passage en vue de reconstituer derrière un tuple et/ou un dictionnaire :
def f (*args, **kwargs):
	print (' '.join(args), kwargs['computer'], kwargs['model'])
def g (*args, **kwargs):
	f (*args, **kwargs)		# Déballage pour l'appel
g ('I', 'love', 'the', computer='Amiga', model='500')
I love the Amiga 500
Les opérateurs étoile (* et **) en Python