Les nouveautés de Python 3.8

Les nouveautés de Python 3.8

Python 3.8 apporte quelques nouveautés intéressantes que je vous présente en détail ici.

Publié le 26 novembre 2019 par Thibault
paceTemps de lecture estimé : 17 minutes

Python 3.8 est sorti le 14 octobre 2019, il y a donc un peu plus d'un mois au moment où j'écris cet article.

Plusieurs nouveautés intéressantes font leur apparition avec cette version de Python.

👉 Dans cet article nous allons voir :

  • Le nouvel opérateur 'walrus'
  • Les arguments positionnels forcés
  • Les nouveautés des f-string

Vous pouvez également vous reporter à l'annonce officielle dans la documentation de Python qui explique en détail (mais en anglais) toutes les nouveautés de Python 3.8.

Les expressions d’affectation (l'opérateur Walrus)

C'est la grosse nouveauté de cette version de Python. En effet, ce n'est pas tous les jours que l'on voit apparaître un nouvel opérateur.

Cet opérateur est représenté par le symbole := et tire son nom de l'animal (morse qui se dit walrus en anglais).

Si vous penchez la tête sur le côté, vous remarquerez en effet une certaine ressemblance.

Cet opérateur permet d'affecter des variables à l'intérieur d'une expression. Auparavant, si on souhaitait assigner une valeur à une variable a et afficher cette valeur, il fallait le faire en deux lignes :

>>> a = 5
>>> print(a)
5

Avec l'opérateur walrus, il est maintenant possible de simplifier ces deux lignes de code :

>>> print(a := 5)
5

Avec un exemple simple comme celui-ci, on ne voit pas forcément tout de suite l'intérêt de ce nouvel opérateur.

Je vais vous montrer quelques exemples dans lesquels notre code peut bénéficier de cette nouvelle notation.

Par exemple, il est assez courant que l'on ait besoin de récupérer une valeur dans une variable, vérifier la valeur de cette variable dans une structure conditionnelle et afficher différents messages en fonction de cette valeur.
Dans le cas de la vérification d'un mot de passe, avec Python 3.7, on aurait fait comme ceci :

password = "abcd123"

if len(password) < 8:
    print(f"Votre mot de passe est trop court ({len(password)} caractères)")

On se retrouve donc à utiliser deux fois la fonction len, ce qui n'est pas très efficace.

On pourrait créer une variable intermédiaire pour stocker la longueur du mot de passe mais on rajouterait encore une ligne de code à ce script pourtant assez simple.

Avec Python 3.8, il est maintenant possible de faire les deux en même temps :

password = "abcd123"

if (longueur_mdp := len(password)) < 8:
    print(f"Votre mot de passe est trop court ({longueur_mdp} caractères)")

☝️Dans le code ci-dessus, on assigne dans la variable longueur_mdp la longueur du mot de passe et on vérifie en même temps si cette valeur est strictement inférieure à 8.

Il paraîtrait d'ailleurs que c'est devenu l'opérateur préféré d'Emmanuel Macron.

Cet opérateur est donc très pratique, au détriment peut-être de la lisibilité. Il est difficile de dire si c'est parce que nous ne sommes pas encore habitués à cette nouvelle syntaxe, mais beaucoup de développeurs Python trouvent que la compréhension du code est plus difficile avec cet opérateur.

Pour vous convaincre cependant de l'utilité que peut avoir cet opérateur, voici d'autres exemples dans lesquels l'opérateur walrus rend le code plus succinct :

Lire un fichier ligne par ligne

Avec Python 3.7

f = open('mon_fichier.txt')
line = f.readline()

while line:
   print(line)
   line = f.readline()

f.close()

Avec Python 3.8

f = open('mon_fichier.txt')

while line := f.readline():
   print(line)

f.close()

Dans ce cas-ci, je trouve la syntaxe avec l'opérateur walrus beaucoup plus agréable à lire !

À l'intérieur d'une boucle while

C'est un exemple que je montre souvent dans mes formations, dans lequel on demande à l'utilisateur s'il veut continuer une opération ou sortir de la boucle.

Avec Python 3.7

while continuer == 'o':
    print('On continue')
    continuer = input('Voulez-vous continuer ? o/n ')

Avec Python 3.8

while (continuer := input('Voulez-vous continuer ? o/n ')) == 'o':
    print('On continue')

À l'intérieur d'un f-string

Il est également possible d'utiliser cet opérateur directement à l'intérieur d'un f-string !

>>> a = 5
>>> b = 10

>>> print(f"Le résultat de la multiplication est égal à {(c := a * b)}")
>>> print(c)
15
Pour que cela fonctionne, il faut entourer l'expression à l'intérieur des accolades avec des parenthèses.

Avec une fonction (contre exemple)

Il est tout à fait possible également d'assigner une valeur retournée dans une fonction avec l'opérateur walrus.

def calcul_reduction(prix):
   return prix * 0.75

prix_maximum = 60

if (prix_reduit := calcul_reduction(100)) > prix_maximum:
   print(f"Désolé mais cet article coûte trop cher ({prix_reduit}€) malgré la réduction.")

On peut donc récupérer dans une variable le prix réduit de l'article, vérifier s'il est supérieur au prix maximum, et si c'est le cas, afficher le prix dans une phrase présentée à l'utilisateur.

C'est cependant tout à fait le cas de figure que je trouve plus complexe à comprendre avec cette syntaxe. Personnellement, je préfère dans un cas comme celui-ci me passer de l'opérateur walrus et organiser mon code de cette façon que je trouve plus claire :

def calcul_reduction(prix):
   return prix * 0.75

prix_maximum = 60
prix_reduit = calcul_reduction(100)

if prix_reduit > prix_maximum:
   print(f"Désolé mais cet article coûte trop cher ({prix_reduit}€) malgré la réduction.")

Il convient donc d'utiliser cet opérateur avec précaution et de ne pas trop en abuser.

Les arguments positionnels 'forcés'

Cette fonctionnalité s'appelle en anglais positional-only arguments, que plusieurs sites ont traduit par arguments positionnels uniquement.

Je trouve cette expression quelque peu difficile à comprendre et traduite littéralement de l'anglais. Dans la suite de cet article, j'utiliserai donc le terme d'arguments positionnels forcés. Les arguments positionnels forcés existaient déjà dans certaines fonctions builtins de Python, comme la fonction pow :

Help on built-in function pow in module builtins:

pow(x, y, z=None, /)
    Equivalent to x**y (with two arguments) or x**y % z (with three arguments)
    
    Some types, such as ints, are able to use a more efficient algorithm when
    invoked using the three argument form.

Remarquez le / à la fin de la ligne pow(x, y, z=None, /).

C'est ce slash qui indique que les arguments précédant le slash sont des arguments positionnels forcés.

Vous ne pouvez donc pas appeler cette fonction comme ceci :

>>> pow(x=5, y=10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pow() takes no keyword arguments

Python nous indique que la fonction pow n'accepte aucun argument nommé.

Vous êtes donc obligés d'appeler cette fonction sans spécifier le nom des arguments : pow(5, 10).

Et il est maintenant possible de faire la même chose avec vos propres fonctions. C'est un cas de figure qui arrive assez rarement, mais il est intéressant d'avoir maintenant cette possibilité.

Pour ce faire, il suffit donc de rajouter un / après les arguments que vous souhaitez comme uniquement positionnels :

def addition(a, b, /):
    return a + b

Ainsi, vous serez obligé d'appeler cette fonction sans spécifier les arguments.

addition(5, 10) fonctionnera, mais pas addition(a=5, b=10).

À savoir que vous pouvez très bien mettre des arguments non positionnels après le slash :

def calcul(a, b, /, mult):
    return (a + b) * mult

>>> calcul(5, 10, 2)
30
>>> calcul(5, 10, mult=10)
150

Vous avez donc le choix de spécifier ou non le nom de l'argument mult lors de l'appel de la fonction, ce qui n'est pas le cas des arguments a et b.

L'erreur que Python vous retournera vous indique d'ailleurs clairement ce que vous avez fait de mal :

def calcul(a, b, /, mult):
   return (a + b) * mult

>>> calcul(5, b=10, mult=10)
TypeError: calcul() got some positional-only arguments passed as keyword arguments: 'b'
Saviez-vous qu'il existe également l'inverse ? À savoir ce que l'on pourrait appeler en français des arguments nommés forcés (keywords-only arguments en anglais) ?

Alors ça peut surprendre pour un langage comme Python qui est réputé pour être assez permissif et vous vous demandez certainement dans quel cas de figure cela peut être utile.

On retrouve souvent ces arguments positionnels forcés dans des fonctions qui utilisent des arguments qui peuvent prendre des noms similaires.

Quand on fait des maths, il arrive souvent que l'on utilise x et y pour spécifier des coordonnées, mais dans un autre contexte on pourrait très bien utiliser a et b, ou encore des constantes comme t pour le temps ou i et n pour spécifier un nombre.

Cela peut être très utile donc si vous souhaitez pouvoir refactorer votre fonction après coup, sans risquer de briser le code des gens qui utilisent votre API.

Imaginez que vous souhaitiez faire une fonction qui affiche des coordonnées x, y et z :

def coordinates(x, y, z):
    return f"x={x}, y={y}, z={z}"

print(coordinates(5, 2, 4))

Vous sortez une première version de votre API et tous vos collègues commencent à utiliser votre fonction en l'appelant comme suit : coordinates(x=5, y=2, z=3).

Si jamais, pour une raison ou une autre, vous décidez par la suite de modifier la signature de votre fonction et de remplacer les arguments x, y et z par a, b et c, il faudra que tous vos collègues modifient leur code pour ne pas causer d'erreurs.

En forçant dès le début vos collègues à ne pas pouvoir utiliser d'arguments nommés, vous prévenez ainsi le risque d'erreur :

def coordinates(x, y, z, /):
    return f"x={x}, y={y}, z={z}"

print(coordinates(5, 2, 4))

☝️ Avec la fonction ci-dessus, vous serez obligé d'appeler la fonction sans spécifier les noms des arguments : coordinates(5, 2, 4).

Ainsi, vous pourrez par la suite changer x, y et z par a, b et c sans risquer de faire planter les scripts de vos collègues.

Comme quoi, les limitations peuvent parfois avoir des côtés positifs !

Les nouveautés des f-string

Vous avez remarqué dans la fonction que j'ai prise comme exemple dans la partie précédente, comme il était pénible de devoir écrire le nom de la variable que l'on inclue dans un f-string ?

def coordinates(x, y, z, /):
    return f"x={x}, y={y}, z={z}"

print(coordinates(5, 2, 4))

En plus de devoir écrire plus de code, vous conviendrez que c'est assez difficile à lire.

Python 3.8 apporte une nouvelle fonctionnalité intéressante à ce niveau, notamment dans les phases de debug.

Pour cela, il vous suffit de mettre un symbole = après la variable que vous souhaitez afficher dans un f-string :

def coordinates(x, y, z, /):
    return f"{x=}, {y=}, {z=}"

>>> coordinates(5, 2, 4)
x=5, y=2, z=4

Cela aura pour effet d'afficher vos variables sous le format nom=valeur.

Un autre exemple :

def afficher_bonjour(prenom, nom):
    print(f"{prenom=}, {nom=}")

>>> afficher_bonjour("John", "Smith")
prenom='John', nom='Smith'

Quand on passe beaucoup de temps à déboguer des scripts et qu'on veut rapidement avoir un affichage de différentes variables c'est donc très pratique pour s'y retrouver facilement !

Il faut également noter que l'intégralité de ce qui se retrouve avant le symbole `=` sera affiché. Donc si vous utilisez plusieurs fonctions pour faire un calcul, le détail du calcul sera affiché. Il est également possible d'utiliser les syntaxes spécifiques des f-string (comme `.3f` pour n'afficher que trois décimales).

>>> from math import cos, radians, pi
>>> f"{cos(radians(pi))=:.3f}"
'cos(radians(pi))=0.998'

☝️ici, on utilise le symbole = directement après le calcul, ainsi que la syntaxe .3f pour n'afficher que trois décimales.

Les autres nouveautés

Voilà pour les trois nouveautés principales de cette version, mais comme à chaque nouvelle version de Python, des centaines de fonctionnalités ont été ajoutées modifiées, voire enlevées.

Parmi les nouveautés intéressantes, il y a notamment l'ajout d'un SyntaxWarning, qui vous avertira dans certains cas de figures qui pourraient ne pas être intentionnels (notamment lors de la vérification d'égalité avec is à la place de ==).

La fonction shutil.copytree() accepte maintenant" }} l'argument dirs_exist_ok que l'on retrouvait déjà dans la fonction os.makedirs et qui permet de ne pas faire planter votre script si un dossier existe déjà.

Il est également possible d'afficher un dictionnaire dans l'ordre d'insertion des clés avec la fonction pprint du module pprint en passant la valeur False à l'argument sort_dicts.

Un nouveau module importlib.metadata permet d'afficher des informations à propos d'un package installé.

Et pour terminer, voici quelques liens (en anglais) pour continuer votre lecture :
Les fonctionnalités dépréciées
Les optimisations
Conseils pour rendre vos scripts compatibles avec Python 3.8