Parmi les nouveautés apportées par la version 3.8 de Python, l’une des principales est l’apparition des expressions d’affectations. Grâce à un nouvel opérateur bien pratique appelé Walrus, il est désormais facile d’économiser quelques lignes de code dans plein de situations simples, et ce sans rajouter de complexité.

Description de l’opérateur Walrus

Le nouvel opérateur en question s’écrit « := ». Sa forme figurant la tête d’un morse sur le côté, il fut rapidement surnommé l’opérateur « walrus » (« morse » dans la langue de Shakespeare). Il permet d’affecter une valeur à une variable au beau milieu d’une autre expression, en créant cette variable à la volée si nécessaire. Démonstration :

>>> print(test := "Hello world!")
Hello world!
>>> test, type(test)
('Hello world!', <class 'str'>)

On constate que non seulement le print se déroule normalement, comme si l’expression d’affectation n’était pas là, mais qu’on a aussi créé une variable test de type str initialisée avec la valeur « Hello world! ». Cette création et cette initialisation sont donc des effets de bord de la première instruction. Dans ce premier exemple trivial, nous avons économisé une ligne de code, puisque l’équivalent sans expression d’affectation aurait été :

>>> test = "Hello world!"
>>> print(test)
Hello world!
>>> test, type(test)
('Hello world!', <class 'str'>)

Exemples d’utilisation

Dans la réalité quotidienne des développeurs, comme l’a noté Guido van Rossum, il n’est pas rare de se retrouver confronté à (ou de produire soi-même) du code où ce n’est pas l’efficacité du code qui est privilégiée, mais sa concision. Le PEP 572 associé au nouvel opérateur donne un exemple typique : pour économiser un niveau d’indentation et une ligne de code, on peut vouloir écrire

match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
	result = match1.group(1)
elif match2:
	result = match2.group(2)
else:
	result = None

au lieu de :

match1 = pattern1.match(data)
if match1:
result = match1.group(1)
else:
match2 = pattern2.match(data)
if match2:
       result = match2.group(2)
else:
       result = None

(qui ne crée match2 que si son usage est nécessaire). Avec des expressions d’affectation, il est désormais possible d’avoir les deux avantages à la fois (et même de réduire encore le nombre de lignes), en écrivant :

if (match1 : = pattern1.match(data)):
result = match1.group(1)
elif (match2 := pattern2.match(data)):
result = match2.group(2)
else:
result = None

Un autre exemple notable est celui des boucles infinies. Quand on souhaite parcourir un fichier ligne par ligne, on peut écrire :

while True:
	line = my_file.readline()
	if not line:
	      break
	# do something with the line

Ceci se réécrirait alors avantageusement :

while (line := my_file.readline()):
	# do something with the line

Ces exemples se retrouvent également dans un post Twitter de Victor Stinner, un des core developer de Python :

Tweet de Victor Stinner

Parmi les autres usages intéressants qu’on peut faire de cet opérateur, on peut aussi citer les deux types suivants :

[y := f(x), y**2, y**3]

[y for x in data if (y := f(x)) is not None]

Quelques remarques

Le PEP énumère un certain nombre de cas où les parenthèses autour de l’expression d’affectation sont obligatoires afin d’éviter des ambiguïtés. Par exemple :

y0 = y1 := f(x)      # Interdit
y0 = (y1 := f(x))   # Valide, bien que déconseillé

foo(x = y := f(x))    # Interdit
foo(x=(y := f(x)))   # Valide, bien que peu lisible 

Concernant la portée des variables affectées, celle-ci est presque toujours la portée courante, c’est-à-dire qu’il n’y a pas d’ambiguïté. La seule exception à cette règle concerne les compréhensions, où la variable appartient en réalité à la portée parente. Cela permet deux choses :

  • D’abord, on peut réutiliser le résultat d’un appel à all ou any en-dehors de la fonction :
	if any(y := f(x) for x in stuff):
	     z = g(y)
  • Ensuite, on peut modifier une variable existant déjà dans la portée parente :
	y = 0
	total = [y := y +f(x) for x in stuff]
	assert y != 0 or not stuff

Conclusion

On comprend donc bien que l’opérateur walrus peut être une manière pratique de réduire la longueur du code et de le rendre plus lisible. Attention toutefois à l’utiliser judicieusement, car il est aussi possible de rendre le code moins clair dans certains cas. Ce dernier point est relativement subjectif et sera donc à discuter entre les membres d’un même projet. N’hésitez pas à consulter le PEP, qui est facile à lire, pour plus de détails et d’explications sur les limitations introduites et les recommandations d’utilisation.

Retrouvez l’ensemble des nouveautés Python sur notre blog !