Vue d'ensemble

Python 3.8, publié le 14 octobre 2019, introduit l'opérateur morse (:=), l'une des additions les plus débattues de l'histoire de Python. Cette version apporte également les paramètres positionnels uniquement, le débogage avec f-string = et TypedDict.

Malgré les controverses autour de l'opérateur morse (qui ont mené à la démission de Guido van Rossum en tant que BDFL), Python 3.8 est une version solide qui offre des outils pratiques pour écrire un code plus expressif et plus sûr. functools.cached_property et les améliorations du typage complètent un ensemble cohérent.

Nouveautés majeures

Opérateur morse := (PEP 572)

L'opérateur d'affectation par expression :=, surnommé « opérateur morse » (walrus operator) en raison de sa ressemblance avec un morse vu de côté, permet d'assigner une valeur à une variable tout en l'utilisant dans une expression. Il évite les calculs redondants et rend certains motifs de code plus concis.

python
# Boucle while avec assignation dans la condition
import re

# Avant Python 3.8 : variable temporaire avant la boucle
# ligne = input("Commande : ")
# while ligne != "quitter":
#     traiter(ligne)
#     ligne = input("Commande : ")

# Avec l'opérateur morse : plus concis
# while (ligne := input("Commande : ")) != "quitter":
#     traiter(ligne)

# Compréhension de liste avec filtrage
# Avant : le calcul est effectué deux fois
donnees_brutes = ["42", "abc", "17", "", "99", "xyz", "8"]

def valider(valeur):
    """Renvoie l'entier si valide, None sinon."""
    try:
        return int(valeur)
    except ValueError:
        return None

# Avec := on évite d'appeler valider() deux fois
valides = [v for x in donnees_brutes if (v := valider(x)) is not None]
print(valides)  # [42, 17, 99, 8]

# Exemple pratique : analyse de fichier journal
lignes_journal = [
    "2024-01-15 INFO  Serveur démarré sur le port 8080",
    "2024-01-15 DEBUG Vérification de santé OK",
    "2024-01-15 ERROR Connexion à la base de données refusée",
    "2024-01-15 WARN  Mémoire utilisée à 85%",
    "2024-01-15 ERROR Timeout lors de la requête /api/users",
]

motif_erreur = re.compile(r"(\d{4}-\d{2}-\d{2})\s+ERROR\s+(.+)")

erreurs = [
    (m.group(1), m.group(2))
    for ligne in lignes_journal
    if (m := motif_erreur.match(ligne))
]
print(erreurs)
# [('2024-01-15', 'Connexion à la base de données refusée'),
#  ('2024-01-15', 'Timeout lors de la requête /api/users')]

# Lecture par blocs d'un fichier
# with open("gros_fichier.bin", "rb") as f:
#     while (bloc := f.read(8192)):
#         traiter_bloc(bloc)

Paramètres positionnels uniquement (PEP 570)

Le séparateur / dans la signature d'une fonction marque les paramètres qui le précèdent comme « positionnels uniquement » : ils ne peuvent pas être passés par nom. C'est un outil puissant pour la conception d'API, car il permet de renommer les paramètres internes sans casser le code des utilisateurs.

python
# Le / sépare les paramètres positionnels des autres
def puissance(base, exposant, /, *, modulo=None):
    """Calcule base ** exposant, comme pow() intégré."""
    resultat = base ** exposant
    if modulo is not None:
        resultat %= modulo
    return resultat

# Fonctionne
print(puissance(2, 10))              # 1024
print(puissance(2, 10, modulo=100))   # 24

# Ne fonctionne PAS (paramètres positionnels uniquement)
# puissance(base=2, exposant=10)  # TypeError!

# Conception d'API pratique
def rechercher(requete, /, *, limite=10, tri="pertinence",
               filtres=None):
    """Recherche avec un premier paramètre positionnel libre."""
    print(f"Recherche de '{requete}' (limite={limite}, tri={tri})")
    if filtres:
        print(f"  Filtres : {filtres}")

# L'utilisateur ne peut pas écrire requete=...
# On peut donc renommer 'requete' en 'terme' sans casser l'API
rechercher("python asyncio", limite=5, tri="date")
# Recherche de 'python asyncio' (limite=5, tri=date)

# Combinaison de tous les types de paramètres
def formatage_complet(positif_seul, /, normal, *, nomme_seul):
    """Démontre les trois catégories de paramètres."""
    print(f"{positif_seul=}, {normal=}, {nomme_seul=}")

formatage_complet(1, 2, nomme_seul=3)          # OK
formatage_complet(1, normal=2, nomme_seul=3)   # OK
# formatage_complet(positif_seul=1, normal=2, nomme_seul=3)  # TypeError!

Débogage avec f-string =

Python 3.8 ajoute le spécificateur = dans les f-strings : en écrivant f"{expr=}", Python affiche à la fois l'expression et sa valeur. C'est un outil de débogage simple mais extrêmement pratique qui évite d'écrire répétitivement le nom de la variable.

python
# Auto-documentation des expressions
x = 42
y = 3.14
nom = "Python"
print(f"{x=}")    # x=42
print(f"{y=}")    # y=3.14
print(f"{nom=}")  # nom='Python'

# Fonctionne avec des expressions complexes
items = ["pomme", "banane", "cerise", "datte"]
print(f"{len(items)=}")           # len(items)=4
print(f"{items[0].upper()=}")     # items[0].upper()='POMME'
print(f"{sum(range(10))=}")       # sum(range(10))=45

# Compatible avec le formatage
import math
print(f"{math.pi=:.4f}")   # math.pi=3.1416
print(f"{1000000=:_}")      # 1000000=1_000_000

# Workflow de débogage pratique
def calculer_remise(prix, quantite, code_promo=None):
    """Calcule une remise avec débogage intégré."""
    sous_total = prix * quantite
    remise = 0.0

    if quantite >= 10:
        remise += 0.05  # 5% pour les gros volumes
    if code_promo == "PROMO20":
        remise += 0.20

    montant_final = sous_total * (1 - remise)

    # Débogage rapide : affiche chaque étape
    print(f"  {prix=}, {quantite=}, {code_promo=}")
    print(f"  {sous_total=:.2f}, {remise=:.0%}")
    print(f"  {montant_final=:.2f}")

    return montant_final

calculer_remise(29.99, 12, "PROMO20")
#   prix=29.99, quantite=12, code_promo='PROMO20'
#   sous_total=359.88, remise=25%
#   montant_final=269.91

TypedDict (PEP 589)

TypedDict permet de déclarer le type des valeurs d'un dictionnaire clé par clé. C'est particulièrement utile pour typer les réponses d'API JSON, les fichiers de configuration et toute structure dict où chaque clé a un type de valeur différent.

python
from typing import TypedDict, List, Optional

# Définir la structure d'une réponse API
class Adresse(TypedDict):
    rue: str
    ville: str
    code_postal: str
    pays: str

class Utilisateur(TypedDict):
    id: int
    nom: str
    email: str
    age: Optional[int]
    adresse: Adresse
    roles: List[str]

# Utilisation avec des données JSON décodées
def afficher_profil(utilisateur: Utilisateur) -> None:
    """Affiche le profil d'un utilisateur typé."""
    print(f"Nom : {utilisateur['nom']}")
    print(f"Email : {utilisateur['email']}")
    print(f"Ville : {utilisateur['adresse']['ville']}")
    print(f"Rôles : {', '.join(utilisateur['roles'])}")

profil: Utilisateur = {
    "id": 1,
    "nom": "Marie Curie",
    "email": "marie@example.com",
    "age": 35,
    "adresse": {
        "rue": "12 rue des Sciences",
        "ville": "Paris",
        "code_postal": "75005",
        "pays": "France",
    },
    "roles": ["admin", "chercheur"],
}

afficher_profil(profil)
# Nom : Marie Curie
# Email : marie@example.com
# Ville : Paris
# Rôles : admin, chercheur

# TypedDict avec total=False (clés optionnelles)
class OptionsRecherche(TypedDict, total=False):
    limite: int
    page: int
    tri: str
    filtres: dict

# Toutes les clés sont optionnelles
opts: OptionsRecherche = {"limite": 20}
print(opts)  # {'limite': 20}

functools.cached_property

Le décorateur functools.cached_property transforme une méthode en propriété dont le résultat est calculé une seule fois puis mis en cache. C'est idéal pour les calculs coûteux qui ne changent pas durant la vie de l'objet : connexions à une base de données, chargement de fichiers de configuration, etc.

python
from functools import cached_property
import time

# Simulation d'un pool de connexions à une base de données
class ServiceBaseDeDonnees:
    """Service avec connexion paresseuse à la base de données."""

    def __init__(self, hote, port, nom_base):
        self.hote = hote
        self.port = port
        self.nom_base = nom_base
        print(f"Service créé (pas encore de connexion)")

    @cached_property
    def pool_connexions(self):
        """Crée le pool de connexions (appelé une seule fois)."""
        print(f"Création du pool vers {self.hote}:{self.port}...")
        time.sleep(0.1)  # Simule un délai de connexion
        return {
            "hote": self.hote,
            "port": self.port,
            "base": self.nom_base,
            "taille": 5,
            "actif": True,
        }

    @cached_property
    def schema(self):
        """Charge le schéma de la base (appelé une seule fois)."""
        print("Chargement du schéma...")
        return ["utilisateurs", "commandes", "produits", "logs"]

    def requete(self, sql):
        """Exécute une requête en utilisant le pool."""
        pool = self.pool_connexions  # Créé au premier appel
        print(f"Exécution sur {pool['base']} : {sql}")

service = ServiceBaseDeDonnees("db.example.com", 5432, "production")
# Service créé (pas encore de connexion)

service.requete("SELECT count(*) FROM utilisateurs")
# Création du pool vers db.example.com:5432...
# Exécution sur production : SELECT count(*) FROM utilisateurs

service.requete("SELECT * FROM commandes LIMIT 10")
# Exécution sur production : SELECT * FROM commandes LIMIT 10
# (pas de recréation du pool !)

# Comparaison avec @property classique
class Ancien:
    @property
    def couteux(self):
        print("Calcul coûteux...")  # Appelé à chaque accès !
        return 42

class Nouveau:
    @cached_property
    def couteux(self):
        print("Calcul coûteux...")  # Appelé une seule fois
        return 42

Améliorations mineures

  • Le protocole __init_subclass__ supporte désormais les arguments de mots-clés.
  • La fonction math.prod() calcule le produit d'un itérable (analogue à sum()).
  • math.isqrt() calcule la racine carrée entière.
  • statistics.NormalDist est ajouté pour les calculs de distribution normale.
  • Le module multiprocessing peut utiliser SharedMemory pour le partage de mémoire inter-processus.
  • Le compilateur Python génère désormais des avertissements de syntaxe plus utiles (ex : SyntaxWarning pour is utilisé avec des littéraux).

Dépréciations et suppressions

Changements notables :

  • L'utilisation de is et is not avec certains littéraux génère désormais un SyntaxWarning.
  • Les collections abstraites de collections (comme collections.Mapping) ne sont plus accessibles directement ; il faut passer par collections.abc.
  • Le type de retour de threading.Thread.isAlive() est déprécié au profit de is_alive().
  • Le paramètre loop de la plupart des fonctions asyncio est déprécié.

Sources