Vue d'ensemble
Python 3.7, publié le 27 juin 2018, est une version particulièrement appréciée de la communauté. Elle introduit les dataclasses, un outil élégant pour créer des classes de données sans le code répétitif habituel. La fonction breakpoint() simplifie le débogage et le module contextvars apporte des variables de contexte thread-safe.
Cette version officialise également l'ordre d'insertion des dictionnaires comme faisant partie de la spécification du langage. Avec l'évaluation différée des annotations et importlib.resources, Python 3.7 consolide les fondations posées par les versions précédentes.
Nouveautés majeures
dataclasses (PEP 557)
Le décorateur @dataclass génère automatiquement les méthodes __init__, __repr__, __eq__ et d'autres à partir des annotations de classe. C'est une alternative plus légère et flexible aux namedtuple et aux classes écrites manuellement.
from dataclasses import dataclass, field
from typing import List
# Dataclass de base
@dataclass
class ArticleInventaire:
nom: str
prix_unitaire: float
quantite: int = 0
@property
def valeur_totale(self) -> float:
return self.prix_unitaire * self.quantite
clavier = ArticleInventaire("Clavier mécanique", 89.99, 15)
souris = ArticleInventaire("Souris ergonomique", 49.99, 30)
print(clavier)
# ArticleInventaire(nom='Clavier mécanique', prix_unitaire=89.99, quantite=15)
print(f"Valeur stock clavier : {clavier.valeur_totale:.2f} €")
# Valeur stock clavier : 1349.85 €
# Égalité automatique
copie = ArticleInventaire("Clavier mécanique", 89.99, 15)
print(clavier == copie) # True
# field() et default_factory pour les types mutables
@dataclass
class Commande:
client: str
articles: List[ArticleInventaire] = field(default_factory=list)
reference: str = field(default="", repr=False)
_total_cache: float = field(default=0.0, init=False, repr=False)
def ajouter(self, article: ArticleInventaire) -> None:
self.articles.append(article)
@property
def total(self) -> float:
return sum(a.valeur_totale for a in self.articles)
cmd = Commande("Dupont SA")
cmd.ajouter(clavier)
cmd.ajouter(souris)
print(f"Total commande : {cmd.total:.2f} €")
# Total commande : 2849.55 €
# Héritage de dataclasses
@dataclass
class ArticlePerissable(ArticleInventaire):
date_expiration: str = ""
temperature_max: float = 4.0
yaourt = ArticlePerissable("Yaourt bio", 1.20, 200, "2024-03-15", 6.0)
print(yaourt)
# ArticlePerissable(nom='Yaourt bio', prix_unitaire=1.2,
# quantite=200, date_expiration='2024-03-15', temperature_max=6.0)
# Comparaison avec namedtuple
from collections import namedtuple
# namedtuple : immuable, pas de valeurs par défaut simples
Point2D = namedtuple("Point2D", ["x", "y"])
p = Point2D(3, 4)
# p.x = 5 # AttributeError : immuable !
# dataclass : mutable par défaut, valeurs par défaut, méthodes
@dataclass
class Point3D:
x: float = 0.0
y: float = 0.0
z: float = 0.0
p3 = Point3D(3, 4)
p3.z = 5 # Fonctionne !
print(p3) # Point3D(x=3, y=4, z=5)
breakpoint() (PEP 553)
La fonction intégrée breakpoint() simplifie le lancement du débogueur. Au lieu d'importer manuellement pdb et d'appeler pdb.set_trace(), un simple appel à breakpoint() suffit. L'avantage supplémentaire est le contrôle via la variable d'environnement PYTHONBREAKPOINT.
# Avant Python 3.7
# import pdb; pdb.set_trace() # Fastidieux à taper
# Python 3.7+ : simple et configurable
def rechercher_doublon(elements):
"""Recherche les doublons dans une liste."""
vus = set()
doublons = []
for elem in elements:
if elem in vus:
# breakpoint() # Décommenter pour déboguer ici
doublons.append(elem)
vus.add(elem)
return doublons
donnees = ["alpha", "beta", "gamma", "beta", "delta", "alpha"]
print(rechercher_doublon(donnees)) # ['beta', 'alpha']
# Contrôle via PYTHONBREAKPOINT :
#
# Désactiver tous les breakpoints :
# PYTHONBREAKPOINT=0 python mon_script.py
#
# Utiliser un autre débogueur (ex: ipdb) :
# PYTHONBREAKPOINT=ipdb.set_trace python mon_script.py
#
# Utiliser le débogueur web (ex: web-pdb) :
# PYTHONBREAKPOINT=web_pdb.set_trace python mon_script.py
contextvars (PEP 567)
Le module contextvars fournit des variables de contexte qui sont implicitement transmises à travers les appels asynchrones. Contrairement aux variables globales ou aux threading.local(), les ContextVar fonctionnent correctement avec asyncio et les coroutines.
import contextvars
import asyncio
# Variable de contexte pour suivre l'ID de requête
request_id: contextvars.ContextVar[str] = contextvars.ContextVar(
"request_id", default="inconnu"
)
# Fonction utilitaire qui utilise le contexte
def log_message(niveau, message):
"""Journalise un message avec l'ID de requête courant."""
rid = request_id.get()
print(f"[{niveau}] [{rid}] {message}")
# Simulation de traitement de requêtes asynchrones
async def traiter_requete(rid, donnees):
"""Traite une requête en propageant son ID dans le contexte."""
request_id.set(rid)
log_message("INFO", f"Début du traitement de '{donnees}'")
await asyncio.sleep(0.1) # Simule un traitement
log_message("INFO", f"Traitement terminé pour '{donnees}'")
async def serveur():
"""Simule un serveur traitant plusieurs requêtes."""
await asyncio.gather(
traiter_requete("REQ-001", "page accueil"),
traiter_requete("REQ-002", "profil utilisateur"),
traiter_requete("REQ-003", "liste produits"),
)
# asyncio.run(serveur())
# [INFO] [REQ-001] Début du traitement de 'page accueil'
# [INFO] [REQ-002] Début du traitement de 'profil utilisateur'
# [INFO] [REQ-003] Début du traitement de 'liste produits'
# [INFO] [REQ-001] Traitement terminé pour 'page accueil'
# ...
# Copie de contexte pour isoler les modifications
ctx = contextvars.copy_context()
# Les modifications dans ctx n'affectent pas le contexte courant
Évaluation différée des annotations (PEP 563)
Grâce à from __future__ import annotations, les annotations de types ne sont plus évaluées au moment de la définition de la fonction mais sont stockées sous forme de chaînes. Cela résout le problème des références circulaires et améliore les performances au démarrage.
from __future__ import annotations
# Sans cet import, cette classe provoquerait une NameError
# car Noeud n'est pas encore défini quand on l'utilise
class Noeud:
"""Noeud d'un arbre binaire avec références en avant."""
def __init__(self, valeur: int,
gauche: Noeud | None = None,
droite: Noeud | None = None):
self.valeur = valeur
self.gauche = gauche
self.droite = droite
def inserer(self, valeur: int) -> Noeud:
"""Insère une valeur et renvoie le noeud modifié."""
if valeur < self.valeur:
if self.gauche is None:
self.gauche = Noeud(valeur)
else:
self.gauche.inserer(valeur)
else:
if self.droite is None:
self.droite = Noeud(valeur)
else:
self.droite.inserer(valeur)
return self
# Références croisées entre classes
class Entreprise:
nom: str
employes: list[Employe] # Référence en avant
class Employe:
nom: str
employeur: Entreprise # Référence en arrière
# Les annotations sont accessibles comme chaînes
print(Noeud.__init__.__annotations__)
# {'valeur': 'int', 'gauche': 'Noeud | None', ...}
importlib.resources
Le module importlib.resources fournit une API propre pour accéder aux fichiers de données intégrés dans les paquets Python. C'est une alternative moderne et fiable à pkg_resources (setuptools), qui est lourd et lent au démarrage.
# Structure du paquet :
# mon_paquet/
# __init__.py
# donnees/
# config_defaut.json
# modele.sql
# Ancien style avec pkg_resources (lent, déprécié)
# import pkg_resources
# chemin = pkg_resources.resource_filename('mon_paquet', 'donnees/config.json')
# Nouveau style avec importlib.resources
from importlib import resources
# Lire un fichier texte depuis un paquet
# contenu = resources.read_text('mon_paquet.donnees', 'config_defaut.json')
# Lire un fichier binaire
# octets = resources.read_binary('mon_paquet.donnees', 'modele.sql')
# Utiliser un fichier temporaire (pour les bibliothèques C, etc.)
# with resources.path('mon_paquet.donnees', 'modele.sql') as chemin:
# print(f"Fichier temporaire : {chemin}")
# # Utiliser chemin comme un vrai fichier sur disque
# Lister les ressources disponibles
# fichiers = resources.contents('mon_paquet.donnees')
# print(list(fichiers))
Améliorations mineures
- L'ordre d'insertion des dictionnaires est désormais garanti par la spécification du langage (et non plus seulement un détail d'implémentation CPython).
- Le module
asynciogagneasyncio.run(), point d'entrée simplifié. - Le module
timeajoute des fonctions nanosecondes :time.time_ns(),time.perf_counter_ns(), etc. - Les
dictsupportent l'ordre inversé avecreversed(). - Amélioration significative du temps de démarrage de l'interpréteur (10 à 30 % plus rapide).
Dépréciations et suppressions
Changements notables :
- Le module
asynciodéprécie les fonctions basées sur les callbacks au profit des coroutines. asyncetawaitdeviennent officiellement des mots-clés réservés.- Le format
StopIterationdans les générateurs provoque désormais uneRuntimeError(PEP 479). - Les boucles d'événements asyncio implicites sont dépréciées.
