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.
# 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.
# 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.
# 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.
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.
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.NormalDistest ajouté pour les calculs de distribution normale.- Le module
multiprocessingpeut utiliserSharedMemorypour le partage de mémoire inter-processus. - Le compilateur Python génère désormais des avertissements de syntaxe plus utiles (ex :
SyntaxWarningpourisutilisé avec des littéraux).
Dépréciations et suppressions
Changements notables :
- L'utilisation de
isetis notavec certains littéraux génère désormais unSyntaxWarning. - Les collections abstraites de
collections(commecollections.Mapping) ne sont plus accessibles directement ; il faut passer parcollections.abc. - Le type de retour de
threading.Thread.isAlive()est déprécié au profit deis_alive(). - Le paramètre
loopde la plupart des fonctions asyncio est déprécié.
