Cours
Python 3.8 a introduit un nouvel opérateur dans le langage. Son nom officiel est expression d’affectation, et elle utilise l’opérateur :=.
Toutefois, on l’appelle plus couramment opérateur morse car il ressemble aux défenses d’un morse !
L’ajout de cet opérateur a suscité la controverse dans le monde habituellement paisible de Python, et son usage reste relativement limité. Il n’en demeure pas moins que l’expression d’affectation fait désormais partie de Python et peut s’avérer utile dans certains cas.
Ce tutoriel explique le fonctionnement de l’opérateur morse de Python et présente des situations où il peut être pertinent.
Devenez ingénieur en données
Qu’est-ce que l’opérateur morse de Python
L’ expression d’affectation, qui utilise l’opérateur :=, ressemble à la plus courante instruction d’affectation, qui utilise l’opérateur =. Cependant, il existe une différence importante entre les deux.
Une instruction (statement) est une unité de code qui exécute une action. Une expression est une instruction qui peut être évaluée en une valeur. Ainsi, une expression renvoie une valeur, tandis que les instructions qui ne sont pas des expressions ne renvoient aucune valeur.
Le REPL de Python (Read, Evaluate, Print, Loop) affiche la valeur d’une expression dès qu’elle est évaluée. En revanche, aucune valeur n’est affichée lorsqu’une instruction qui n’est pas une expression est exécutée :
>>> import random # Not an expression
>>> 10 * 2 # An expression
20
L’instruction import ne s’évalue à aucune valeur. En revanche, 10 * 2 est une expression qui s’évalue en une valeur.
L’instruction d’affectation, qui utilise l’opérateur =, assigne un objet à une variable ou une autre référence, mais ne renvoie pas la valeur. L’expression d’affectation, ou opérateur morse, effectue la même opération que l’instruction d’affectation tout en renvoyant l’objet :
>>> value = 10 # Assignment statement
>>> value
10
>>> (value := 20) # Assignment expression
20
Notez que l’instruction d’affectation ne renvoie pas la valeur, contrairement à l’expression d’affectation. Ainsi, une expression d’affectation combine plusieurs opérations :
- Elle évalue l’expression à droite de l’opérateur
:=. - Elle assigne cette valeur à un nom de variable.
- Elle renvoie la valeur.
Les parenthèses sont requises dans cet usage de l’expression d’affectation à la place d’une instruction d’affectation, comme dans cet exemple, afin d’éviter toute ambiguïté. Nous reviendrons plus loin sur les exigences syntaxiques de l’opérateur morse.
Vous pouvez utiliser une expression d’affectation partout où des expressions sont autorisées en Python. Voyons un exemple :
print(f"The number is {(value := 50)}")
print(value)
The number is 50
50
Le nom de variable value est défini à l’intérieur de la f-string de la première ligne. Mais l’entier est aussi renvoyé directement dans les accolades de la f-string. Les accolades d’une f-string doivent contenir une expression, ce qui explique pourquoi une instruction d’affectation ne peut pas être utilisée.
Cas d’usage de l’opérateur morse de Python
Deux cas d’usage fréquents de l’opérateur morse sont la simplification et l’optimisation du code.
Simplifier le code
Un usage courant de l’opérateur morse est de simplifier du code où une variable devrait normalement être définie avant d’être utilisée, par exemple dans certaines conditions. Considérez la boucle while suivante :
import random
value = random.randint(1, 20)
while value < 18:
print(value)
value = random.randint(1, 20)
7
5
1
11
12
1
9
Ce code exécute le bloc while tant que la valeur aléatoire générée est inférieure à 18. La variable value doit être définie avant la boucle while pour pouvoir être utilisée dans l’instruction while. Un nouveau nombre aléatoire est affecté à value à la fin du bloc while.
L’expression d’affectation := permet de réduire le nombre de lignes et d’appeler la fonction random.randint() en un seul endroit du programme :
import random
while (value := random.randint(1, 20)) < 18:
print(value)
1
9
2
4
9
15
11
Un nouveau nombre aléatoire est généré et affecté à value à chaque nouvelle itération de la boucle while.
Optimiser le code
Voyons un autre exemple avec des compréhensions de liste. Considérez le code suivant, qui filtre une liste de chaînes de caractères contenant des dates et crée une liste d’datetime.datetime objets ne conservant que celles d’une année donnée :
from datetime import datetime
def format_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
format_date(date)
for date in dates
if format_date(date).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
La fonction format_date() accepte une chaîne au format yyyy-mm-dd et renvoie un objet datetime.datetime. L’un des attributs de datetime.datetime est .year, qui contient l’année de la date.
Seules les trois dates de 2024 figurent dans la liste finale, qui contient des objets datetime.datetime plutôt que des chaînes. La compréhension de liste inclut une clause if, qui appelle format_date(). Or, format_date() est aussi appelée dans la première expression qui génère les valeurs à ajouter à la liste. Appeler deux fois la même fonction avec le même argument est inefficace, surtout si cette fonction constitue un goulot d’étranglement pour les performances.
Une option pour éviter cet appel en double consiste à appeler la fonction et à l’affecter à une variable avant utilisation. Toutefois, cela n’est pas possible via une instruction d’affectation tout en conservant une compréhension de liste. Cette solution impose une boucle for classique :
from datetime import datetime
def format_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = []
for date in dates:
formatted_date = format_date(date)
if formatted_date.year == 2024:
formatted_dates.append(formatted_date)
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
L’opérateur morse offre une alternative compatible avec une compréhension de liste, puisque la date formatée renvoyée par la fonction peut être affectée à un nom de variable et renvoyée dans la même expression :
from datetime import datetime
def format_date(date_str):
return datetime.strptime(date_str, "%Y-%m-%d")
dates = ["2024-01-01", "2022-12-31", "2024-06-15", "2023-08-23", "2024-11-30"]
formatted_dates = [
formatted_date
for date in dates
if (formatted_date := format_date(date)).year == 2024
]
print(formatted_dates)
[datetime.datetime(2024, 1, 1, 0, 0), datetime.datetime(2024, 6, 15, 0, 0), datetime.datetime(2024, 11, 30, 0, 0)]
L’expression d’affectation dans la compréhension de liste est l’expression entre parenthèses, (formatted_date := format_date(date)). Elle appelle la fonction format_date(), assigne sa valeur à la variable formatted_date et renvoie cette valeur. Comme l’expression d’affectation s’évalue en un objet datetime.datetime, le code accède à l’attribut .year, comparé à 2024 avec l’opérateur d’égalité.
La clause if de la compréhension de liste est évaluée avant l’expression qui génère la valeur à stocker dans la liste. L’opérateur morse est donc placé dans la clause if, et la variable définie par cette expression d’affectation est utilisée comme première expression de la compréhension.
La controverse autour de l’opérateur morse de Python
L’expression d’affectation ne fait pas l’unanimité chez les développeurs Python. Certains y voient un moyen de simplifier le code, d’autres estiment qu’elle nuit à la lisibilité et préfèrent les options plus verbeuses. La lisibilité étant en partie subjective, les avis divergent sur le fait que l’opérateur morse rende le code plus ou moins clair.
Le dernier exemple de la section précédente illustre le compromis nécessaire. Sans l’opérateur morse, ce code exige d’initialiser d’abord une liste vide, puis d’y ajouter des valeurs dans une boucle for. Cette solution est plus longue et moins efficace qu’une compréhension de liste, optimisée pour ce cas.
Cependant, la solution avec boucle for standard peut sembler plus lisible à certains programmeurs qui préfèrent écrire davantage de code plutôt que d’utiliser l’opérateur morse, surtout si les performances ne sont pas un enjeu.
De nombreux guides de style recommandent encore d’éviter l’expression d’affectation, ou de ne l’utiliser qu’avec parcimonie.
Opérateur morse de Python : règles de syntaxe
Il est fréquent de rencontrer des erreurs de syntaxe lors des premières expérimentations avec l’opérateur morse, en raison de ses règles spécifiques.
Pour éviter toute confusion entre l’instruction d’affectation = et l’expression d’affectation :=, il n’existe aucun cas où les deux seraient syntaxiquement valides au même endroit. Pour cette raison, il est souvent nécessaire d’entourer l’expression d’affectation de parenthèses :
(value := 20) # Valid
value := 20 # Invalid (SyntaxError)
Dans cette situation, l’obligation d’inclure des parenthèses autour de l’expression d’affectation est l’inverse de la règle concernant l’instruction d’affectation, qui, elle, ne peut pas être placée entre parenthèses.
Cependant, l’expression d’affectation n’exige pas toujours de parenthèses :
import random
if value := random.randint(0, 3):
print(f"{value} is greater than 0")
1 is greater than 0
L’expression d’affectation dans l’instruction if génère un nombre aléatoire entre 0 et 3 et l’assigne à la variable value. Les entiers 1, 2 et 3 sont truthy, et le code du bloc if s’exécutera lorsque ces valeurs sont générées. En revanche, quand random.randint() renvoie 0, valeur falsy, le bloc if n’est pas exécuté.
L’instruction d’affectation n’est pas valable après un mot-clé if. Il n’y a donc pas de confusion dans ce cas, et les parenthèses ne sont pas nécessaires.
Cet exemple illustre aussi un autre point clé concernant l’expression d’affectation. La portée de la variable affectée suit les règles LEGB normales. La variable value est donc disponible partout dans la portée globale dans cet exemple :
import random
if value := random.randint(0, 3):
print(f"{value} is greater than 0")
print(value)
2 is greater than 0
2
Si une expression d’affectation est utilisée dans la définition d’une fonction, la variable ne sera disponible qu’en local dans cette fonction. C’est le même comportement que pour les variables créées par une instruction d’affectation.
On peut modifier cet exemple pour montrer un autre écueil possible avec l’opérateur morse. Ajoutons un opérateur de comparaison dans l’instruction if :
import random
if value := random.randint(0, 3) < 2:
print(f"{value} is less than 2")
True is less than 2
La sortie met en évidence le problème : value vaut le booléen True et non un entier. L’opérateur morse a la plus faible priorité (precedence) de tous les opérateurs (seule la virgule a une priorité inférieure). L’opérateur « inférieur à » < est donc évalué en premier et son résultat est affecté à value. Des parenthèses sont nécessaires pour corriger cela :
import random
if (value := random.randint(0, 3)) < 2:
print(f"{value} is less than 2")
0 is less than 2
L’expression d’affectation est désormais évaluée en premier puisque les parenthèses ont la priorité la plus élevée. Le nombre renvoyé par random.randint() est affecté à value et sert d’opérande gauche à l’opérateur « inférieur à ».
Contrairement à l’instruction d’affectation, l’opérateur morse ne peut affecter des valeurs qu’à des noms de variables : il est impossible de l’utiliser pour affecter une valeur à un indice ou à un attribut :
(name := "James") # Valid
points = {"James": 10, "Mary": 20}
(points["Sarah"] := 30) # Invalid
class Article:
pass
article = Article()
(article.title := "The Walrus Operator") # Invalid
On ne peut pas affecter une valeur via un indice ni à un attribut avec l’opérateur morse.
Conclusion
L’expression d’affectation, qui utilise la syntaxe := et que l’on appelle souvent opérateur morse, est une nouveauté relativement récente de Python. L’instruction d’affectation, qui utilise l’opérateur =, reste la manière la plus courante d’affecter une valeur à un nom de variable.
Cependant, l’opérateur morse étend cette opération en renvoyant également la valeur affectée, ce qui en fait une expression. Vous pouvez donc utiliser l’expression d’affectation partout où les autres expressions sont autorisées en Python.
La communauté Python reste toutefois partagée quant à l’utilité et la lisibilité de l’opérateur morse. C’est pourquoi l’expression d’affectation est moins utilisée que d’autres nouveautés du langage. Il est utile de savoir s’en servir, mais évitez d’en abuser !
Devenez ingénieur en données
J'ai étudié la physique et les mathématiques au niveau UG à l'université de Malte. J'ai ensuite déménagé à Londres et obtenu un doctorat en physique à l'Imperial College. J'ai travaillé sur de nouvelles techniques optiques pour obtenir des images de la rétine humaine. Aujourd'hui, je me concentre sur l'écriture, la communication et l'enseignement de Python.
