Les nouveautés de la version 3.11 de Python
- 2022-10-25
- Publié par : Christophe DELEUZE
- Catégorie : Python
Python 3.11 est là et je vous propose de passer en revue la majorité des nouvelles fonctionnalités intéressantes de ce cru. Prendre le temps de suivre les nouveautés et les évolutions des langages de programmation que vous utilisez est vraiment quelque chose d’important, tant pour la culture que pour l’expérience. Si au moins un élément de cet article attise votre curiosité, foncez et n’hésitez pas à l’approfondir par vous-même afin de bien comprendre comment cela fonctionne et à quoi cela sert.
De 10% jusqu'à 60 % plus rapide que la version précédente
Python 3.11 devrait être jusqu’à 60 % plus rapide que Python 3.10, selon votre charge de travail. En moyenne, on s’attend à ce qu’il soit 25 % plus rapide que les versions précédentes en termes de démarrage et d’exécution.
Comparatif d'un même code exécuté en 3.10 et en 3.11
import time
start = time.perf_counter()
lst = [i**2 for i in range(10000000)]
end = time.perf_counter()
print(f"Le temps d'exécution est de {end-start:.2f} secondes.")
Résultat avec python 3.10
Le temps d'exécution est de 5,50 secondes.
Résultat avec python 3.11
Le temps d'exécution est de 2,14 secondes.
Qu'est ce qui a permis cette amélioration ?
Python 3.11 est la première version à bénéficier d’un projet appelé Faster CPython, où CPython est la version standard de l’interpréteur.
Faster CPython est un projet financé par Microsoft, dont les membres comprennent l’inventeur du Python Guido van Rossum, l’ingénieur logiciel senior de Microsoft Eric Snow et Mark Shannon – qui est sous contrat avec Microsoft en tant que responsable technique du projet.
L’amélioration de vitesse provient principalement d’une technique consistant à spécialiser les instructions d’un code dont les appels sont répétitifs. Étant donné que le type d’un objet change rarement, l’interpréteur tente maintenant d’analyser le code en cours d’exécution et de remplacer les codes (bytecodes) génériques par des codes spécifiques au type. Par exemple, les opérations binaires (addition, soustraction, etc.) peuvent être remplacées par des versions spécialisées pour les entiers, les flottants et les chaînes. À noter que cette amélioration de performance se fait au prix de diverses optimisations qui ont toutes un coût en mémoire nécessaire afin de stocker des éléments pour les utiliser plus tard et d’être capable de gérer les cas où une version non optimisée serait demandée pour le débogage.
Une autre des principales astuces d’optimisation consiste à réduire le nombre d’appels à l’allocateur système en faveur de l’allocation d’un plus grand espace. Plutôt que de gérer de multiples petits espaces de mémoire, l’allocateur alloue un plus grand espace et se décharge de la gestion de celui-ci. En réduisant les appels à l’allocateur, on est capable de nettement améliorer la vitesse.
Ce qu’il faut retenir avec Faster CPython :
- Python 3.11 retrouve une vitesse équivalente à la version la plus rapide de Python qui était la 2.7 ;
- Garder toujours à l’esprit la bonne pratique qui consiste à ne pas changer le type de vos variables en cours de route.
PEP 657: Amélioration de la lisibilité des traces d'erreurs
Pour démontrer cette nouvelle fonctionnalité, nous allons utiliser deux exemples. Nous exécuterons le premier exemple avec Python 3.10 et le deuxième avec python 3.11. Les deux codes génèreront une erreur et nous regarderons la différence dans les messages d’erreur.
def division(a: int, b: int):
c = a/b
return c
print(division(10, 0))
Python 3.10
ZeroDivisionError: division by zero
Python 3.11
c = a/b
~^~
ZeroDivisionError: division by zero
Les deux codes ci-dessus génèrent une erreur de division par zéro. Le premier exemple ne renvoie que la ligne avec l’erreur (c = a/b) mais il ne précise pas exactement ce qui cause l’erreur. Dans le deuxième exemple (utilisant python 3.11), non seulement il renvoie la ligne avec l’erreur, mais il pointe également vers l’élément réel du code à l’origine de l’erreur, le signe division (/).
Ajout de nouveaux types pour aller plus loin dans le typage
Petit rappel, depuis la version 3.5 de Python, les fonctionnalités d’indication de type ont fait leur apparition en Python. Ces fonctionnalités d’indication de type facilitent la gestion et l’analyse de codes source plus volumineux. Chaque version depuis Python 3.5 à vu son lot d’ajout pour pouvoir améliorer le typage en Python. Comme pour chaque version, python 3.11 apporte plusieurs nouveaux ajouts d’indication de type dont la gestion du type Self, LiteralString, Required et NotRequired.
PEP 673: Le type Self
Les méthodes de classe qui retournent comme valeur leur propre classe (self), nécessitaient auparavant des annotations complexes et détaillées pour être utiles. Self vous permet d’annoter la valeur de retour d’une méthode de classe, comme simplement, Self ce qui simplifiera aussi le travail des outils d’analyse pour ce type de méthodes.
Dans les versions précédentes de Python, si vous vouliez annoter des méthodes qui renvoient l’instance de la classe, vous deviez utiliser la classe TypeVar du module de typage. Python 3.11 a ajouté une nouvelle façon intuitive d’annoter les méthodes sans utiliser l’approche basée sur TypeVar. Le premier code ci-dessous montre comment l’approche basée sur TypeVar est implémentée. Le deuxième code illustre la nouvelle approche python 3.11 utilisant le type Self.
from typing import TypeVar
TVoiture = TypeVar("TVoiture", bound="Voiture")
class Voiture:
def modele(self: TVoiture, model: str) -> TVoiture:
self.model = model
return self
Avec python 3.11, pour annoter une méthode d’une classe, utiliser simplement le type Self est suffisant. Vous pouvez voir ci-dessous que cette approche rend le code plus concis et facile à comprendre.
from typing import Self
class Voiture:
def modele(self: Self, model: str) -> Self:
self.model = model
return self
PEP 675: Le type Arbitrary LiteralString
Auparavant, les annotations de type n’avaient aucun moyen d’indiquer qu’une variable donnée devait être un littéral de chaîne, c’est-à-dire une chaîne définie dans le code source. La nouvelle annotation typing.LiteralString corrige cela. En utilisant la nouvelle annotation, les linters peuvent tester si une variable est soit une chaîne définie dans la source, soit une nouvelle chaîne composée uniquement de chaînes définies par la source.
Prenons le même exemple que dans la release note, illustrant l’appel à une base de données :
def run_query(sql: LiteralString) -> ...
...
def caller(
arbitrary_string: str,
query_string: LiteralString,
table_name: LiteralString,
) -> None:
run_query("SELECT * FROM students") # ok
run_query(query_string) # ok
run_query("SELECT * FROM " + table_name) # ok
run_query(arbitrary_string) # type check error
run_query(f"SELECT * FROM students WHERE name = {arbitrary_string}") # type check error
Dans cet exemple, la fonction run_query n’accepte que des LiteralString, donc si on lui donne une chaîne arbitraire, cela entraînera mécaniquement une erreur de type. Ainsi, votre code est protégé et le linter vous remontera les éventuels effets de bord.
PEP 655: Les types Required et NotRequired des TypedDict
Python 3.8 a introduit et ajouté TypedDict au module de typage. Le type TypedDict permettait de créer un dictionnaire avec des clés et des valeurs spécifiques. Cependant, si nous voulions que certaines informations soient facultatives dans le dictionnaire, ce n’était pas forcément facile à mettre en œuvre. Prenons un exemple pour le démontrer. Nous allons créer un dictionnaire de films dont l’année est facultative. Jusqu’à Python 3.10, pour rendre l’année optionnelle, il était nécessaire de créer une classe enfant comme cela :
from typing import TypedDict
class FilmBase(TypedDict):
titre: str
annee: int
class Film(FilmBase):
annee: int
Python 3.11 introduit les types Required et NotRequired pour contourner ce type de problème. En utilisant ces types, nous n’avons plus besoin de créer une classe enfant pour implémenter une clé facultative, à la place, nous pouvons simplement annoter la clé facultative comme NotRequired, ou inversement, annoté toutes les clés obligatoires comme Required.
from typing import TypedDict, NotRequired
class Film(TypedDict):
titre: str
annee: NotRequired[int]
m1: Film = {"titre": "Black Panther", "annee": 2018} # OK
m2: Film = {"titre": "Star Wars"} # OK, car l'annee n'est pas requise
m3: Film = {"annee": 2022} # ERREUR, car il manque le titre du film
La définition précédente est équivalente à celle-ci :
from typing import TypedDict, Required
class Film(TypedDict, total=False):
titre: Required[str]
annee: int
PEP 654: ExceptionGroups et except*
Python 3.11 inclut un nouveau type d’exception intégré appelé ExceptionGroup. Ce qui est intéressant avec ce type d’exception, c’est qu’il permettra de déclencher plusieurs exceptions ou erreurs différentes en même temps. L’ExceptionGroup prend deux arguments, une chaîne, puis suivi d’une séquence d’erreurs que nous voulons déclencher et gérer. Exemple ci-dessous :
try:
raise ExceptionGroup("ErrorsGroup", ([
ValueError("Valeur invalide"),
TypeError("Type invalide"),
KeyError("Clé invalide"),
NameError("Nom invalide")]))
except* ValueError as ve:
pass
except* TypeError as te:
pass
except* KeyError as ke:
pass
except* NameError as ne:
pass
Une fois que nous avons relevé les erreurs, les nouveaux blocs except* nous permettent de gérer toutes les erreurs individuellement.
Le mot de la fin
Python 3.11 est une release qui promet des performances équivalentes à la mythique 2.7 ! Rien que pour cela, on sent que les migrations vers cette version ne vont pas traîner !
Pour de plus amples détails sur tous les fix et les nouveautés, je vous invite à vous reporter à la documentation :
Release Note de Python 3.11
Pour des questions, points à éclaircir, les commentaires sont là pour ça.
Merci pour cet excellent résumé des nouveautés de Python 3.11. Je regarde souvent ce type d’article de loin, mais celui-ci est très bien documenté. Je le garde sous coude, ainsi que ce blog, pour le recommender à mes étudiants.