Vue d'ensemble

Python 3.5, publié le 13 septembre 2015, est la version qui rend la programmation asynchrone enfin agréable à écrire. Les mots-clés async et await remplacent les décorateurs et les yield from introduits dans Python 3.4, offrant une syntaxe native et lisible pour les coroutines.

Cette version marque aussi l'arrivée des type hints (PEP 484), qui permettent d'annoter les types des paramètres et des valeurs de retour sans impact sur l'exécution. L'opérateur @ pour la multiplication matricielle et le très performant os.scandir() complètent cette version riche en innovations.

Nouveautés majeures

async/await (PEP 492)

Les mots-clés async def et await introduisent une syntaxe native pour définir et appeler des coroutines. Fini le décorateur @asyncio.coroutine et les yield from : le code asynchrone se lit désormais presque comme du code synchrone.

python
import asyncio

# Comparaison Python 3.4 vs 3.5

# Python 3.4 :
# @asyncio.coroutine
# def recuperer(url):
#     reponse = yield from asyncio.sleep(1)
#     return reponse

# Python 3.5 : syntaxe native
async def recuperer_page(url, session_id):
    """Simule la récupération d'une page web."""
    print(f"[Session {session_id}] Récupération de {url}...")
    await asyncio.sleep(0.5)  # simule la latence réseau
    return {"url": url, "taille": len(url) * 100, "statut": 200}

async def traiter_page(url, session_id):
    """Récupère et traite une page."""
    resultat = await recuperer_page(url, session_id)
    print(f"[Session {session_id}] {url} -> {resultat['taille']} octets")
    return resultat

async def crawler(urls):
    """Lance le crawl de plusieurs URLs en parallèle."""
    taches = [
        traiter_page(url, idx)
        for idx, url in enumerate(urls, 1)
    ]
    resultats = await asyncio.gather(*taches)
    total = sum(r["taille"] for r in resultats)
    print(f"\nTotal récupéré : {total} octets sur {len(resultats)} pages")
    return resultats

# Point d'entrée
urls = [
    "https://blog.example.com/article-1",
    "https://blog.example.com/article-2",
    "https://blog.example.com/article-3",
    "https://blog.example.com/article-4",
]
asyncio.run(crawler(urls))

Type hints (PEP 484)

Les type hints permettent d'annoter les types des paramètres et des valeurs de retour des fonctions. Ces annotations sont ignorées à l'exécution mais peuvent être exploitées par des outils d'analyse statique comme mypy, des éditeurs de code et des générateurs de documentation. Le module typing fournit les types génériques comme Optional, Union et List.

python
from typing import Optional, Union, List, Dict, Tuple

# Fonction avec annotations de types
def rechercher_utilisateur(
    identifiant: Union[int, str],
    actif_seulement: bool = True
) -> Optional[Dict[str, Union[str, int]]]:
    """Recherche un utilisateur par ID ou nom d'utilisateur."""
    base = {
        1: {"nom": "Dupont", "prenom": "Alice", "age": 32, "actif": True},
        2: {"nom": "Martin", "prenom": "Bob", "age": 28, "actif": False},
    }
    for uid, info in base.items():
        if uid == identifiant or info["nom"] == identifiant:
            if actif_seulement and not info["actif"]:
                return None
            return info
    return None

# Fonction qui transforme des données avec types
def calculer_statistiques(valeurs: List[float]) -> Dict[str, float]:
    """Calcule des statistiques de base sur une liste de nombres."""
    if not valeurs:
        return {"moyenne": 0.0, "minimum": 0.0, "maximum": 0.0}
    return {
        "moyenne": sum(valeurs) / len(valeurs),
        "minimum": min(valeurs),
        "maximum": max(valeurs),
    }

# Fonction générique avec Tuple
def parser_coordonnees(texte: str) -> Tuple[float, float]:
    """Parse une chaîne 'lat,lon' en tuple de coordonnées."""
    parties = texte.split(",")
    return float(parties[0].strip()), float(parties[1].strip())

# Utilisation
utilisateur = rechercher_utilisateur(1)
print(utilisateur)  # {'nom': 'Dupont', 'prenom': 'Alice', ...}

stats = calculer_statistiques([23.5, 19.8, 31.2, 27.1])
print(stats)  # {'moyenne': 25.4, 'minimum': 19.8, 'maximum': 31.2}

lat, lon = parser_coordonnees("48.8566, 2.3522")
print(f"Paris : {lat}, {lon}")  # Paris : 48.8566, 2.3522

Opérateur @ de multiplication matricielle (PEP 465)

L'opérateur @ est dédié à la multiplication matricielle. Il est implémenté via les méthodes spéciales __matmul__, __rmatmul__ et __imatmul__. Cet opérateur est principalement utilisé par NumPy, mais on peut l'implémenter dans n'importe quelle classe.

python
class Matrice:
    """Matrice 2x2 simple avec support de l'opérateur @."""

    def __init__(self, a, b, c, d):
        # | a  b |
        # | c  d |
        self.a, self.b = a, b
        self.c, self.d = c, d

    def __matmul__(self, autre):
        """Multiplication matricielle avec l'opérateur @."""
        return Matrice(
            self.a * autre.a + self.b * autre.c,
            self.a * autre.b + self.b * autre.d,
            self.c * autre.a + self.d * autre.c,
            self.c * autre.b + self.d * autre.d,
        )

    def __repr__(self):
        return f"Matrice({self.a}, {self.b}, {self.c}, {self.d})"

    def afficher(self):
        print(f"| {self.a:>6.2f}  {self.b:>6.2f} |")
        print(f"| {self.c:>6.2f}  {self.d:>6.2f} |")

# Rotation de 90 degrés
import math
angle = math.radians(90)
rotation = Matrice(
    math.cos(angle), -math.sin(angle),
    math.sin(angle),  math.cos(angle),
)

# Mise à l'échelle x2
echelle = Matrice(2, 0, 0, 2)

# Composition de transformations avec @
transformation = rotation @ echelle
print("Rotation 90° puis échelle x2 :")
transformation.afficher()
# |   0.00  -2.00 |
# |   2.00   0.00 |

# Double rotation
double_rotation = rotation @ rotation
print("\nDouble rotation 90° (= 180°) :")
double_rotation.afficher()
# |  -1.00   0.00 |
# |   0.00  -1.00 |

os.scandir (PEP 471)

os.scandir() est une alternative nettement plus rapide à os.listdir(). Au lieu de renvoyer une simple liste de noms, elle renvoie un itérateur d'objets DirEntry qui fournissent directement les métadonnées du fichier (type, taille, dates) sans appels système supplémentaires. Le gain est spectaculaire sur les répertoires volumineux.

python
import os
import time

# Comparaison os.listdir vs os.scandir

def lister_avec_listdir(repertoire):
    """Liste les fichiers avec os.listdir + os.stat (ancienne méthode)."""
    resultats = []
    for nom in os.listdir(repertoire):
        chemin = os.path.join(repertoire, nom)
        info = os.stat(chemin)  # appel système supplémentaire !
        if os.path.isfile(chemin):
            resultats.append((nom, info.st_size))
    return resultats

def lister_avec_scandir(repertoire):
    """Liste les fichiers avec os.scandir (méthode rapide)."""
    resultats = []
    with os.scandir(repertoire) as entrees:
        for entree in entrees:
            if entree.is_file():  # pas d'appel système suppl.
                info = entree.stat()  # souvent déjà en cache
                resultats.append((entree.name, info.st_size))
    return resultats

# Benchmark sur un répertoire
repertoire = "/usr/lib"

debut = time.perf_counter()
fichiers_v1 = lister_avec_listdir(repertoire)
duree_listdir = time.perf_counter() - debut

debut = time.perf_counter()
fichiers_v2 = lister_avec_scandir(repertoire)
duree_scandir = time.perf_counter() - debut

print(f"os.listdir : {len(fichiers_v1)} fichiers en {duree_listdir:.4f}s")
print(f"os.scandir : {len(fichiers_v2)} fichiers en {duree_scandir:.4f}s")
if duree_listdir > 0:
    ratio = duree_listdir / duree_scandir
    print(f"scandir est {ratio:.1f}x plus rapide")

# Utilisation avancée : trouver les 5 plus gros fichiers
def top_fichiers(repertoire, n=5):
    """Trouve les n plus gros fichiers d'un répertoire."""
    fichiers = []
    with os.scandir(repertoire) as entrees:
        for entree in entrees:
            if entree.is_file(follow_symlinks=False):
                fichiers.append((entree.name, entree.stat().st_size))
    fichiers.sort(key=lambda f: f[1], reverse=True)
    return fichiers[:n]

print(f"\nTop 5 fichiers dans {repertoire} :")
for nom, taille in top_fichiers(repertoire):
    print(f"  {nom:40s} {taille:>10,} octets")

Améliorations mineures

  • L'opérateur de décompression généralisé : a, *b, c = [1, 2, 3, 4, 5] fonctionne désormais dans plus de contextes (PEP 448).
  • Les dictionnaires et ensembles littéraux supportent le dépaquetage : {**d1, **d2}.
  • Le module zipapp permet de créer des applications Python exécutables en un fichier .pyz.
  • Le module math gagne math.isclose() pour comparer des flottants avec une tolérance.
  • bytes et bytearray supportent le formatage avec %.

Dépréciations et suppressions

Changements notables :

  • Le module asyncio n'est plus provisoire : son API est désormais stabilisée.
  • Le module formatter reste déprécié et sera retiré dans une version future.
  • Les anciennes coroutines basées sur @asyncio.coroutine restent supportées mais la nouvelle syntaxe async/await est recommandée.

Sources