Vue d'ensemble

Python 3.10, publié le 4 octobre 2021, est l'une des versions les plus attendues de ces dernières années. Elle introduit le structural pattern matching (match/case), une fonctionnalité inspirée des langages fonctionnels qui enrichit considérablement les possibilités d'expression du langage.

Au-delà du pattern matching, cette version améliore significativement les messages d'erreur, autorise les gestionnaires de contexte entre parenthèses et renforce le système de typage avec TypeAlias et ParamSpec.

Nouveautés majeures

Pattern matching structurel (PEP 634/635/636)

Le pattern matching structurel est la nouveauté phare de Python 3.10. L'instruction match/case permet de comparer une valeur à une série de motifs, avec capture de variables, conditions de garde et déstructuration des données. C'est bien plus puissant qu'un simple switch/case d'autres langages.

python
# Exemple 1 : parseur de commandes
def executer_commande(commande):
    """Interprète une commande texte."""
    match commande.split():
        case ["quitter"]:
            print("Au revoir !")
            return False
        case ["aide"]:
            print("Commandes : quitter, aide, ouvrir <fichier>, chercher <motif> [dans <fichier>]")
        case ["ouvrir", fichier]:
            print(f"Ouverture de {fichier}")
        case ["chercher", motif]:
            print(f"Recherche de '{motif}' dans tous les fichiers")
        case ["chercher", motif, "dans", fichier]:
            print(f"Recherche de '{motif}' dans {fichier}")
        case _:
            print(f"Commande inconnue : {commande}")
    return True

executer_commande("ouvrir rapport.txt")
# Ouverture de rapport.txt
executer_commande("chercher erreur dans journal.log")
# Recherche de 'erreur' dans journal.log
python
# Exemple 2 : traitement de données JSON structurées
def traiter_evenement(evenement):
    """Traite un événement selon sa structure."""
    match evenement:
        case {"type": "connexion", "utilisateur": nom}:
            print(f"Connexion de {nom}")
        case {"type": "achat", "produit": produit, "quantite": q} if q > 10:
            print(f"Achat en gros : {q}x {produit}")
        case {"type": "achat", "produit": produit, "quantite": q}:
            print(f"Achat : {q}x {produit}")
        case {"type": "erreur", "code": code, "message": msg}:
            print(f"Erreur {code} : {msg}")
        case _:
            print(f"Événement non géré : {evenement}")

traiter_evenement({"type": "connexion", "utilisateur": "Alice"})
# Connexion de Alice
traiter_evenement({"type": "achat", "produit": "clavier", "quantite": 25})
# Achat en gros : 25x clavier
traiter_evenement({"type": "achat", "produit": "souris", "quantite": 3})
# Achat : 3x souris
python
# Exemple 3 : pattern matching sur des classes
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Cercle:
    centre: Point
    rayon: float

@dataclass
class Rectangle:
    origine: Point
    largeur: float
    hauteur: float

def decrire_forme(forme):
    """Décrit une forme géométrique."""
    match forme:
        case Cercle(centre=Point(x=0, y=0), rayon=r):
            print(f"Cercle centré à l'origine, rayon {r}")
        case Cercle(centre=Point(x=cx, y=cy), rayon=r) if r > 100:
            print(f"Grand cercle en ({cx}, {cy}), rayon {r}")
        case Cercle(centre=c, rayon=r):
            print(f"Cercle en ({c.x}, {c.y}), rayon {r}")
        case Rectangle(largeur=l, hauteur=h) if l == h:
            print(f"Carré de côté {l}")
        case Rectangle(largeur=l, hauteur=h):
            print(f"Rectangle {l}x{h}")

decrire_forme(Cercle(Point(0, 0), 5))
# Cercle centré à l'origine, rayon 5
decrire_forme(Rectangle(Point(1, 2), 10, 10))
# Carré de côté 10

Gestionnaires de contexte entre parenthèses (PEP 617)

Grâce au nouveau parseur PEG introduit en Python 3.9, il est désormais possible d'utiliser des parenthèses dans les instructions with pour répartir plusieurs gestionnaires de contexte sur plusieurs lignes de manière lisible.

python
# Avant Python 3.10 : obligation d'utiliser des backslashes
# with open('source.txt') as src, \
#      open('dest.txt', 'w') as dst:
#     dst.write(src.read())

# Python 3.10 : parenthèses autorisées
with (
    open('source.txt') as src,
    open('dest.txt', 'w') as dst,
):
    dst.write(src.read())

# Très utile pour plusieurs fichiers
with (
    open('config.json') as config,
    open('donnees.csv') as donnees,
    open('rapport.txt', 'w') as rapport,
):
    # Traitement utilisant les trois fichiers
    pass

Meilleurs messages d'erreur

Python 3.10 améliore considérablement les messages d'erreur. Les SyntaxError indiquent désormais plus précisément la position du problème, et les messages sont plus explicites. Les parenthèses, crochets et accolades non fermés sont mieux signalés, et les erreurs d'attribut ou de nom incluent des suggestions.

python
# Erreur de parenthèse non fermée :
# resultat = (1 + 2
#            ^
# SyntaxError: '(' was never closed

# Oubli des deux-points dans un if :
# if x > 0
#         ^
# SyntaxError: expected ':'

# Suggestion pour les erreurs de nom :
# nombre = 42
# print(nombe)
# NameError: name 'nombe' is not defined. Did you mean: 'nombre'?

# Suggestion pour les erreurs d'attribut :
# import collections
# collections.ordereddict
# AttributeError: module 'collections' has no attribute 'ordereddict'.
# Did you mean: 'OrderedDict'?

TypeAlias et ParamSpec (PEP 613, PEP 612)

TypeAlias permet de déclarer explicitement qu'une variable est un alias de type, levant l'ambiguïté avec une simple affectation. ParamSpec permet de typer des décorateurs qui préservent la signature de la fonction décorée.

python
from typing import TypeAlias, ParamSpec, Callable, TypeVar
import functools
import time

# TypeAlias : déclarer un alias de type explicite
Coordonnees: TypeAlias = tuple[float, float]
Matrice: TypeAlias = list[list[float]]
Resultat: TypeAlias = dict[str, list[int]]

def distance(a: Coordonnees, b: Coordonnees) -> float:
    """Calcule la distance entre deux points."""
    return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5

print(distance((0, 0), (3, 4)))  # 5.0

# ParamSpec : typer un décorateur préservant la signature
P = ParamSpec('P')
T = TypeVar('T')

def chronometre(func: Callable[P, T]) -> Callable[P, T]:
    """Mesure le temps d'exécution d'une fonction."""
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        debut = time.perf_counter()
        resultat = func(*args, **kwargs)
        duree = time.perf_counter() - debut
        print(f"{func.__name__} : {duree:.4f}s")
        return resultat
    return wrapper

@chronometre
def calculer_somme(n: int) -> int:
    return sum(range(n))

resultat = calculer_somme(1_000_000)
# calculer_somme : 0.0312s

Améliorations mineures

  • int.bit_count() renvoie le nombre de bits à 1 dans la représentation binaire d'un entier.
  • zip() gagne un paramètre strict pour vérifier que les itérables ont la même longueur (PEP 618).
  • dataclasses supporte les champs avec slots=True et kw_only=True.
  • Les types int, str et bytes gagnent en performance.
  • Le module statistics s'enrichit de nouvelles fonctions de corrélation et de régression.

Dépréciations et suppressions

Les changements notables dans cette catégorie :

  • Les alias de type du module typing (typing.Dict, typing.List, etc.) sont dépréciés en faveur des types natifs.
  • Le module distutils est déprécié et prévu pour suppression dans Python 3.12.
  • Les encodages non textuels dans str.encode() sont dépréciés.

Sources