Bonnes pratiques Python¶
Python encourage un style de code lisible et expressif, formalise par une serie de conventions regroupees dans la PEP 8. Au-delà du formatage, Python offre des idiomes puissants (comprehensions, generateurs, context managers) qui permettent d'écrire un code plus concis et plus efficace. Cette section recense les pratiques recommandees et les pieges les plus fréquents.
PEP 8 — Conventions de style¶
PEP 8 est le guide de style officiel de Python. Les points essentiels :
Nommage¶
| Élément | Convention | Exemple |
|---|---|---|
| Variable | snake_case | nombre_articles |
| Fonction | snake_case | calculer_total() |
| Classe | PascalCase | CommandeUtilisateur |
| Constante | UPPER_SNAKE_CASE | TAUX_TVA = 0.20 |
| Module | snake_case | traitement_donnees.py |
| Package | lowercase | monpaquet/ |
| Attribut prive | _prefixe | self._cache |
| Attribut "interne" | __double | self.__secret (name mangling) |
Mise en forme¶
# Longueur de ligne : 88 caracteres (standard ruff/black)
# PEP 8 recommande 79, mais 88-100 est la norme moderne
# Imports : stdlib, tiers, local — separes par une ligne vide
import os
import sys
from pathlib import Path
import fastapi
from sqlalchemy.orm import Session
from monpaquet.models import Item
# Espaces autour des operateurs
x = 10 + 2 * (5 - 1)
# Pas d'espace avant les parentheses d'appel
result = ma_fonction(arg1, arg2)
# Virgule finale dans les structures multi-lignes
config = {
"host": "localhost",
"port": 8000,
"debug": False, # virgule finale = diff plus propre en git
}
Idiomes Python¶
Comprehensions¶
# Liste de carres des nombres pairs
carres_pairs = [x**2 for x in range(20) if x % 2 == 0]
# Dictionnaire inverse (valeur -> cle)
original = {"a": 1, "b": 2, "c": 3}
inverse = {v: k for k, v in original.items()}
# Set de mots uniques en minuscules
texte = "Python est Simple Python est Lisible"
mots_uniques = {mot.lower() for mot in texte.split()}
# Expression generatrice (evaluee paresseusement)
total = sum(x**2 for x in range(1000)) # Ne cree pas de liste intermediaire
Context managers¶
# Ouverture de fichier — fermeture garantie
with open("donnees.csv", encoding="utf-8") as f:
contenu = f.read()
# Plusieurs ressources simultanees
with open("source.txt") as src, open("dest.txt", "w") as dst:
dst.write(src.read())
# Context manager personnalise avec contextlib
from contextlib import contextmanager
import time
@contextmanager
def chronometre(nom: str):
debut = time.perf_counter()
try:
yield
finally:
duree = time.perf_counter() - debut
print(f"{nom}: {duree:.3f}s")
with chronometre("traitement"):
result = sum(range(1_000_000))
Unpacking et spread¶
# Unpacking de sequences
premier, *reste = [1, 2, 3, 4, 5]
# premier = 1, reste = [2, 3, 4, 5]
*debut, dernier = [1, 2, 3, 4, 5]
# debut = [1, 2, 3, 4], dernier = 5
# Echange de variables sans temporaire
a, b = 1, 2
a, b = b, a # a=2, b=1
# Merge de dictionnaires (Python 3.9+)
defaults = {"timeout": 30, "retries": 3}
overrides = {"timeout": 60}
config = {**defaults, **overrides} # Python 3.5+
config = defaults | overrides # Python 3.9+
Generateurs¶
def lire_lignes_csv_par_batch(chemin: str, taille: int = 1000):
"""
Lit un CSV volumineux par lots sans tout charger en memoire.
Utilise un generateur pour une empreinte memoire constante.
"""
import csv
batch: list[dict] = []
with open(chemin, encoding="utf-8") as f:
reader = csv.DictReader(f)
for ligne in reader:
batch.append(ligne)
if len(batch) >= taille:
yield batch
batch = []
if batch: # Dernier lot (possiblement incomplet)
yield batch
# Usage
for lot in lire_lignes_csv_par_batch("ventes.csv", taille=500):
traiter_lot(lot)
Anti-patterns courants¶
Argument mutable par défaut¶
# MAUVAIS : la liste est partagee entre tous les appels
def ajouter_item(item: str, liste: list = []) -> list:
liste.append(item)
return liste
ajouter_item("a") # ["a"]
ajouter_item("b") # ["a", "b"] — surprenant !
# CORRECT : utiliser None comme sentinel
def ajouter_item(item: str, liste: list | None = None) -> list:
if liste is None:
liste = []
liste.append(item)
return liste
Bare except¶
# MAUVAIS : capture toutes les exceptions, y compris KeyboardInterrupt
try:
faire_quelque_chose()
except:
pass
# CORRECT : capter uniquement les exceptions prevues
try:
faire_quelque_chose()
except ValueError as e:
logger.warning("Valeur invalide : %s", e)
except (ConnectionError, TimeoutError) as e:
logger.error("Erreur reseau : %s", e)
raise
État global mutable¶
# MAUVAIS : etat global difficile a tester et a raisonner
_cache = {}
def obtenir_config(cle: str) -> str:
return _cache.get(cle, "")
# CORRECT : encapsuler dans une classe ou utiliser l'injection de dependances
from functools import lru_cache
class ConfigService:
def __init__(self) -> None:
self._cache: dict[str, str] = {}
def obtenir(self, cle: str) -> str:
return self._cache.get(cle, "")
def definir(self, cle: str, valeur: str) -> None:
self._cache[cle] = valeur
Comparaisons incorrectes¶
# MAUVAIS
if x == None:
pass
if x == True:
pass
if type(x) == list:
pass
# CORRECT
if x is None:
pass
if x is True:
pass
if isinstance(x, list):
pass
Gestion d'erreurs — EAFP vs LBYL¶
Python favorise le style EAFP (Easier to Ask Forgiveness than Permission) plutôt que LBYL (Look Before You Leap).
# LBYL (style C/Java) — verifier avant d'agir
if "cle" in dictionnaire and isinstance(dictionnaire["cle"], int):
valeur = dictionnaire["cle"]
else:
valeur = 0
# EAFP (style pythonique) — essayer et gerer l'echec
try:
valeur = dictionnaire["cle"]
except (KeyError, TypeError):
valeur = 0
# Encore plus pythonique avec .get()
valeur = dictionnaire.get("cle", 0)
Exceptions personnalisees¶
class ErreurApplication(Exception):
"""Classe de base pour les erreurs de l'application."""
pass
class ErreurValidation(ErreurApplication):
"""Donnee invalide soumise a l'application."""
def __init__(self, champ: str, message: str) -> None:
self.champ = champ
self.message = message
super().__init__(f"Champ '{champ}': {message}")
class ErreurRessourceIntrouvable(ErreurApplication):
"""Ressource demandee inexistante."""
def __init__(self, type_ressource: str, identifiant: int | str) -> None:
self.type_ressource = type_ressource
self.identifiant = identifiant
super().__init__(f"{type_ressource} {identifiant!r} introuvable")
# Usage
def obtenir_utilisateur(user_id: int) -> dict:
resultat = db.find(user_id)
if resultat is None:
raise ErreurRessourceIntrouvable("Utilisateur", user_id)
return resultat
Performance — slots, caching, generateurs¶
from functools import lru_cache, cache
from dataclasses import dataclass
# __slots__ : reduit la memoire des instances (pas de __dict__)
class PointOptimise:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
# @dataclass avec slots (Python 3.10+)
@dataclass(slots=True, frozen=True)
class Coordonnee:
latitude: float
longitude: float
# Memoisation des appels couteux
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# @cache = @lru_cache(maxsize=None) — sans limite
@cache
def calculer_factorielle(n: int) -> int:
if n == 0:
return 1
return n * calculer_factorielle(n - 1)
Profiler avant d'optimiser
Ne jamais optimiser sans mesure. Utilisez cProfile ou py-spy pour identifier les vrais goulots d'étranglement avant de modifier le code. Voir le chapitre Écosystème pour les outils de profiling.