Maîtriser cinq concepts Python clés accélère la montée en compétence : comprehensions/generators, décorateurs, context managers, type hints/dataclasses et async/concurrency. Cet article explique comment et pourquoi les utiliser, avec exemples pratiques et cas d’usage pour produire du code plus lisible, performant et maintenable.
List comprehensions et generators pourquoi ?
Les list comprehensions rendent le code plus concis et lisible, les generator expressions économisent la mémoire en produisant les éléments à la demande.
Les différences, avantages et pièges.
Les list comprehensions créent immédiatement une liste complète en mémoire. Les generator expressions créent un itérateur « paresseux » (lazy) qui produit un élément quand on le demande. La paresse (lazy evaluation) réduit l’empreinte mémoire pour de grands flux de données, mais empêche l’accès aléatoire et réitérable sans recréation.
- Syntaxe: Exemple général — List comprehension: [expr for x in iterable if cond]. Generator expression: (expr for x in iterable if cond).
- Avantage list: Accès direct, itérable multiple, souvent plus rapide pour petits ensembles.
- Avantage generator: Mémoire minimale, idéal pour pipelines et gros fichiers.
- Piège: sys.getsizeof() mesure l’objet conteneur mais pas toujours les objets référencés (les ints restent comptés séparément).
1) boucle avec append()
nums = []
for i in range(10):
nums.append(i*i)2) équivalent en list comprehension
🚀 Devenez un expert en Data Marketing avec nos formations !
Maîtrisez les outils essentiels pour analyser, automatiser et visualiser vos données comme un pro. De BigQuery SQL à Google Apps Script, de n8n à Airtable, en passant par Google Sheets et Looker Studio, nos formations couvrent tous les niveaux pour vous permettre d’optimiser vos flux de données, structurer vos bases SQL, automatiser vos tâches et créer des dashboards percutants. Que vous soyez débutant ou avancé, chaque formation est conçue pour une mise en pratique immédiate et un impact direct sur vos projets. Ne subissez plus vos données, prenez le contrôle dès aujourd’hui ! 📊🔥
nums = [i*i for i in range(10)]3) generator expression + usage next()
gen = (i*i for i in range(10))
print(next(gen)) # 0
print(list(gen)) # [1, 4, 9, ...]Mesure mémoire (exemple, Python 3.x, 64-bit):
import sys
lst = list(range(1_000_000))
gen = (i for i in range(1_000_000))
print(sys.getsizeof(lst)) # ≈ 8_000_056 (taille du tableau de pointeurs)
print(sys.getsizeof(gen)) # ≈ 112 (objet générateur)Précision: Les ~8 Mo correspondent au tableau de pointeurs de la liste. Les objets int référencés prennent ~28 bytes chacun en CPython, donc la mémoire totale réelle est plus élevée si l’on compte les int.
Cas d’usage: Pour un pipeline de traitement, combiner generators permet de chaîner transformations sans stocker tout en mémoire (map/filter générateurs). Pour du streaming ou la lecture ligne à ligne d’un fichier, utiliser un generator évite de charger l’intégralité du fichier.
| Approche | Concision | Performance mémoire | Cas d’usage |
| Boucle + append() | Moyenne (plus verbeuse) | Élevée (liste en mémoire) | Clarté, modifications étape par étape |
| List comprehension | Très concise | Élevée (liste remplie) | Petits à moyens ensembles, accès multiple |
| Generator expression | Concise | Très faible (éléments à la demande) | Pipelines, streaming, gros volumes |
À quoi servent les décorateurs ?
Les décorateurs permettent d’ajouter du comportement (logging, timing, authentification, caching) sans modifier le code source des fonctions, en favorisant le principe DRY.
Un décorateur est une fonction qui prend une fonction en entrée et renvoie une nouvelle fonction dite « wrapper ». Le wrapper encapsule l’appel original et ajoute du comportement avant/après. Pour préserver l’identité de la fonction décorée (attributs __name__ et __doc__), j’utilise functools.wraps, qui copie les métadonnées de la fonction d’origine.
Exemple de définition d’un timer_decorator avec functools.wraps :
import time
from functools import wraps
def timer_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} exécuté en {end - start:.6f}s")
return result
return wrapper
Exemple d’application sur deux fonctions et sortie affichant le temps :
@timer_decorator
def fast():
sum(range(1000))
@timer_decorator
def slow():
time.sleep(0.2)
fast()
slow()
# Sortie attendue :
# fast exécuté en 0.0000xxs
# slow exécuté en 0.2000xxs
Pour le logging simple, un décorateur peut enregistrer les arguments et la sortie. Pour le caching, Python propose functools.lru_cache, qui mémorise les résultats pour des appels identiques :
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
Bonnes pratiques : Toujours accepter *args/**kwargs dans le wrapper pour conserver la compatibilité. Empiler les décorateurs fonctionne top-down (le décorateur du haut est exécuté en dernier). Gérer les exceptions dans le wrapper si vous modifiez le flux (réessais, fallback). Utiliser wraps pour garder l'introspection et faciliter le debugging.
Avertissements : Les décorateurs peuvent compliquer l'introspection (inspect.signature, outils de profiling) et altérer le typage statique (mypy, annotations). Documenter le comportement ajouté et, si besoin, fournir des signatures explicites ou utiliser typing.overload pour conserver le typage.
| Usage | Bénéfice | Précautions |
| Timer | Mesure simple des performances | Ne pas laisser en production sans filtrage (bruit) |
| Logging | Traçabilité des appels et erreurs | Gérer confidentialité et volume |
| Caching (lru_cache) | Réduction des calculs répétitifs | Consommation mémoire, validité des clés |
Quand utiliser un context manager ?
Un context manager garantit l'acquisition et la libération sûre des ressources (fichiers, connexions, verrous), même en cas d'exception.
Le protocole context manager repose sur deux méthodes :
- __enter__ : Méthode appelée au début du bloc with, qui acquiert la ressource et retourne généralement l'objet à utiliser.
- __exit__ : Méthode appelée en sortie du bloc with, qui reçoit trois arguments liés à l'exception (exc_type, exc_val, exc_tb) et doit libérer la ressource. Retourner True supprime l'exception.
Le mot-clé with est une syntaxe qui garantit l'appel de __exit__ même si une exception survient, ce qui rend le code plus sûr et plus lisible qu'un try/finally explicite.
Exemple sans with (try/finally) pour ouvrir et écrire dans un fichier :
f = open('out.txt', 'w')
try:
f.write('hello')
finally:
f.close()Même opération avec with :
with open('out.txt', 'w') as f:
f.write('hello')Exemple de context manager personnalisé via une classe (connexion fictive) :
class FakeConn:
def __init__(self, addr):
self.addr = addr
self.connected = False
def __enter__(self):
self.connected = True
print(f'Connected to {self.addr}')
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.connected = False
print(f'Disconnected from {self.addr}')
return False # Ne pas supprimer l'exception
with FakeConn('db://localhost') as conn:
# Utiliser conn
passExemple alternatif avec contextlib.contextmanager et yield (fonction génératrice gère l'entrée/sortie) :
from contextlib import contextmanager
@contextmanager
def managed_conn(addr):
conn = FakeConn(addr)
try:
conn.__enter__()
yield conn
finally:
conn.__exit__(None, None, None)
with managed_conn('db://localhost') as c:
passCas d'usage typiques :
- Bases de données : Gestion de transactions et fermeture automatique des connexions.
- Sockets : Fermeture propre des sockets même en cas d'erreur réseau.
- threading.Lock : Acquisition et libération sûres de verrous pour éviter les deadlocks.
- Pipelines data : Allocation temporaire de ressources (fichiers temporaires, workers) et libération garantie entre étapes.
| Approche | Simplicité | Contrôle d'erreur | Cas d'usage |
| try/finally | Moyenne à faible, verbosité accrue. | Bon contrôle mais risque d'oublis humains. | Scripts rapides ou debug où on veut contrôle précis. |
| with (builtin) | Très simple, lisible. | Excellente, garantit release. | Fichiers, connexions simples, verrous. |
| Classe __enter__/__exit__ | Moyenne, nécessite boilerplate. | Excellent, permet logique avancée et suppression d'exceptions. | Ressources complexes, gestion d'état. |
| contextlib.contextmanager | Simple pour wrappers, moins verbeux que la classe. | Bon, usage clair pour flux avec yield. | Petits wrappers, composition, testing. |
Faut-il utiliser type hints et dataclasses ?
Les type hints et les dataclasses améliorent la lisibilité, la maintenance et l'outillage (analyse statique, autocompletion) sans changer le runtime du code.
Les annotations de type sont normalisées par la PEP 484; elles décrivent les types attendus sans enforcement à l'exécution. Elles permettent aux outils comme mypy ou pyright d'identifier des erreurs avant runtime, et elles améliorent l'autocomplétion et la documentation dans l'IDE.
Exemples concrets d'annotations courantes :
from typing import List, Optional
def somme(vals: List[int], label: Optional[str] = None) -> int:
total: int = sum(vals)
if label:
print(label)
return total
TypedDict (du module typing) permet de typer des dicts structurés quand une classe n'est pas nécessaire.
Présentation des dataclasses (module dataclasses) : elles génèrent automatiquement __init__, __repr__, __eq__, etc., et conviennent parfaitement aux objets de données.
from dataclasses import dataclass, field, asdict
from typing import List
@dataclass(frozen=True)
class User:
id: int
name: str
tags: List[str] = field(default_factory=list)
# Conversion dataclass -> dict
user = User(1, "Alice")
data = asdict(user)
Immutabilité : la dataclass définie avec frozen=True lève dataclasses.FrozenInstanceError si on tente de modifier un attribut. La conversion via asdict ne casse pas l'immuabilité.
Impact sur sécurité et runtime : Les annotations sont ignorées à l'exécution par défaut, donc coût négligeable. Pour des vérifications strictes à l'exécution, utiliser des bibliothèques comme pydantic ou des validateurs explicites.
Exemple de commande et explication courte :
$ mypy mymodule.py
# mypy analyse static et signale les incompatibilités de types sans exécuter le code
Bonnes pratiques :
- Annoter progressivement les modules critiques plutôt que tout d'un coup.
- Intégrer mypy ou pyright en CI pour attraper les régressions.
- Préférer dataclasses pour les objets de données simples plutôt qu'un dict non typé.
| Critère | Type Hints | Dataclasses |
| Lisibilité | Élevée (documente les API) | Élevée (structure explicite) |
| Outillage | Très bon (mypy, IDE) | Bon (autocomplétion, sérialisation) |
| Sécurité | Améliore détection d'erreurs statiques | Aide à éviter erreurs de structure |
| Performances | Coût négligeable au runtime | Coût minime (création d'objets) |
Comment gérer l'async et la concurrence ?
Réponse claire : utilisez asyncio pour la concurrence I/O-bound et concurrent.futures (ThreadPoolExecutor/ProcessPoolExecutor) pour les tâches CPU-bound en respectant les limites du GIL.
Distinction essentielle : I/O-bound signifie que le programme attend des opérations externes (réseau, disque). CPU-bound signifie que le travail utilise intensément le processeur. Vous choisirez asyncio quand l'attente domine, et des pools de threads/processus quand le calcul CPU bloque.
Explication rapide :
- Asyncio fonctionne avec une boucle d'événements, des coroutines (async def) et des points d'arrêt (await).
- Le GIL (Global Interpreter Lock) est un verrou qui empêche l'exécution simultanée de bytecode Python dans plusieurs threads; il limite la parallélisation CPU avec des threads.
Version synchrone simulée (blocante avec time.sleep) :
import time
def fetch(i):
time.sleep(1)
return f"result {i}"
def main():
results = []
for i in range(50):
results.append(fetch(i))
print(results)
if __name__ == "__main__":
main()
Version asyncio équivalente (concurrente via asyncio.sleep + gather) :
import asyncio
async def fetch(i):
await asyncio.sleep(1)
return f"result {i}"
async def main():
tasks = [fetch(i) for i in range(50)]
results = await asyncio.gather(*tasks)
print(results)
if __name__ == "__main__":
asyncio.run(main())
Comparaison simplifiée : La version synchrone prendra ≈50s pour 50 tâches de 1s chacune. La version asyncio prendra ≈1s + overhead (parallélisme des sleeps), soit un gain énorme pour I/O-bound.
Exemple ThreadPoolExecutor vs ProcessPoolExecutor pour CPU-bound :
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
def run_threadpool():
with ThreadPoolExecutor() as ex:
print(list(ex.map(fib, [30]*4)))
def run_processpool():
with ProcessPoolExecutor() as ex:
print(list(ex.map(fib, [30]*4)))
Conseils et pièges : utiliser run_in_executor pour exécuter du code bloquant sans bloquer la boucle. Éviter d'appeler des fonctions CPU-intensives dans l'event loop. Gérer les exceptions avec asyncio.gather(..., return_exceptions=True) ou try/except sur les tasks. Ne pas mélanger appels bloquants (requests, time.sleep) sans les isoler.
Bibliothèques courantes : Pour HTTP async, aiohttp est une option répandue et mature (client/serveur async).
| Problème | Outil | Simplicité | Scalabilité |
| I/O-bound (réseau, disque) | asyncio / aiohttp | Élevée une fois le pattern compris | Très bonne pour centaines/milliers de connexions |
| CPU-bound | ProcessPoolExecutor | Moyenne (sérialisation, communication) | Bonne, utilise plusieurs cœurs |
| Blocage simple ou compatibilité | ThreadPoolExecutor ou run_in_executor | Très simple | Limité par le GIL pour le CPU |
Prêt à appliquer ces concepts Python pour vos projets ?
J'ai présenté cinq concepts Python qui transforment du code scolaire en code professionnel : list comprehensions et generators pour la concision et la mémoire, décorateurs pour réutiliser du comportement, context managers pour gérer les ressources, type hints et dataclasses pour l'outillage et la maintenabilité, et asyncio/concurrency pour l'évolutivité. En appliquant ces patterns vous réduisez les bugs, facilitez la revue de code et optimisez les performances. Si vous voulez, je peux vous aider à auditer et refactorer votre code pour en tirer ces bénéfices concrets.
FAQ
-
Qu'est‑ce qu'un generator et quand l'utiliser ?
Un generator produit des éléments à la demande, ce qui réduit la mémoire utilisée pour de grands flux de données. À privilégier pour le streaming de fichiers, les pipelines de données et toute boucle traitant beaucoup d'éléments. -
Comment écrire un décorateur sans casser l'introspection ?
Utiliser functools.wraps dans le wrapper du décorateur pour préserver __name__, __doc__ et les annotations, ce qui conserve l'introspection et l'outillage d'IDE. -
Pourquoi préférer with à try/finally ?
with centralise l'acquisition/libération de ressources et garantit la libération même en cas d'exception, rendant le code plus propre et moins sujet aux fuites. -
Les type hints sont‑ils obligatoires en production ?
Non, mais ils améliorent la lisibilité, permettent l'analyse statique (mypy/pyright) et réduisent les erreurs. Ils sont particulièrement utiles sur les bases de code équipes ou critiques. -
Asyncio remplace-t-il le multithreading pour tout ?
Non. asyncio est excellent pour les tâches I/O-bound. Pour les tâches CPU-bound, on utilisera ProcessPoolExecutor (multi‑processus) ou des solutions distribuées à cause du GIL.
A propos de l'auteur
Franck Scandolera — expert & formateur en Tracking avancé server-side, Analytics Engineering, Automatisation No/Low Code (n8n), intégration de l'IA et SEO/GEO. Responsable de l'agence webAnalyste et de l'organisme de formation Formations Analytics. Références clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Française de Football, Texdecor. Dispo pour aider les entreprises => contactez moi.
⭐ Analytics engineer, Data Analyst et Automatisation IA indépendant ⭐
- Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
- Data Analyst & Analytics engineering : tracking avancé (GA4, Matomo, Piano, GTM server, Tealium, Commander Act, e-commerce, CAPI, RGPD), entrepôt de données (BigQuery, Snowflake, PostgreSQL, ClickHouse), modèles (Airflow, dbt, Dataform), dashboards décisionnels (Looker, Power BI, Metabase, SQL, Python).
- Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
- Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.





