Apprendre Python de zéro jusqu'au niveau expert : syntaxe, structures de données, POO, générateurs, décorateurs, typage, tests, async et internals du langage.
C'est ici que Python devient vraiment Python. Ces quatre mécanismes sont partout dans les frameworks — FastAPI, pytest, Django, SQLAlchemy — et les comprendre transforme la lecture de leur code source.
Un itérable possède __iter__() qui renvoie un itérateur. Un itérateur possède __next__() qui renvoie l'élément suivant ou lève StopIteration.
nombres = [1, 2, 3]
it = iter(nombres)
next(it) # 1
next(it) # 2
next(it) # 3
next(it) # StopIterationUn for fait exactement ça en boucle. On peut donc rendre n'importe quel objet itérable :
class CompteARebours:
def __init__(self, depart: int):
self.depart = depart
def __iter__(self):
n = self.depart
while n > 0:
yield n
n -= 1
for i in CompteARebours(3):
print(i) # 3, 2, 1Le yield dans __iter__ en fait un générateur, beaucoup plus court qu'implémenter __next__ à la main.
Une fonction qui contient yield devient un générateur. Elle s'exécute jusqu'au yield, rend la valeur, se met en pause, et reprend au next() suivant.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
from itertools import islice
list(islice(fibonacci(), 10)) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Une suite infinie, en mémoire constante. C'est l'intérêt principal : l'évaluation paresseuse.
Remplacez les crochets d'une compréhension par des parenthèses :
total = sum(x ** 2 for x in range(10_000_000)) # pas de liste de 10M élémentsEnchaîner des générateurs permet de traiter un fichier de 50 Go avec quelques Mo de RAM :
def lire(chemin):
with open(chemin, encoding="utf-8") as f:
yield from f
def erreurs(lignes):
return (l for l in lignes if " 500 " in l)
def ips(lignes):
return (l.split()[0] for l in lignes)
from collections import Counter
Counter(ips(erreurs(lire("access.log")))).most_common(5)yield from délègue à un autre itérable.
from itertools import chain, groupby, batched, pairwise, product, accumulate
list(chain([1, 2], [3])) # [1, 2, 3]
list(batched(range(7), 3)) # [(0, 1, 2), (3, 4, 5), (6,)] — Python 3.12+
list(pairwise([1, 2, 3])) # [(1, 2), (2, 3)]
list(accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]
list(product("ab", [0, 1])) # [('a', 0), ('a', 1), ('b', 0), ('b', 1)]
ventes = sorted(ventes, key=lambda v: v["mois"])
for mois, groupe in groupby(ventes, key=lambda v: v["mois"]):
print(mois, sum(v["montant"] for v in groupe))groupby ne regroupe que les éléments consécutifs : triez d'abord.
Un décorateur est une fonction qui prend une fonction et renvoie une fonction. La syntaxe @ n'est que du sucre :
@decorateur
def f(): ...
# équivaut à
f = decorateur(f)Un décorateur de chronométrage :
import time
from functools import wraps
def chrono(func):
@wraps(func)
def wrapper(*args, **kwargs):
debut = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__} : {time.perf_counter() - debut:.3f}s")
return wrapper
@chrono
def calcul_lourd(n):
return sum(range(n))
calcul_lourd(10_000_000)@wraps recopie le nom et la docstring de la fonction d'origine. Sans lui, toutes vos fonctions décorées s'appellent wrapper dans les tracebacks.
Il faut un niveau d'imbrication supplémentaire : une fonction qui renvoie un décorateur.
def retry(tentatives: int = 3, delai: float = 1.0, exceptions=(Exception,)):
def decorateur(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(1, tentatives + 1):
try:
return func(*args, **kwargs)
except exceptions:
if i == tentatives:
raise
time.sleep(delai * 2 ** (i - 1))
return wrapper
return decorateur
@retry(tentatives=5, exceptions=(ConnectionError,))
def appeler_api():
...from functools import cache, lru_cache
@cache
def fib(n: int) -> int:
return n if n < 2 else fib(n - 1) + fib(n - 2)
fib(200) # instantané grâce à la mémoïsation@lru_cache(maxsize=128) borne la taille du cache. Attention : les arguments doivent être hashables.
Le with appelle __enter__ à l'entrée et __exit__ à la sortie, même en cas d'exception. On l'utilise pour tout ce qui doit être libéré : fichiers, verrous, connexions, transactions.
class Transaction:
def __init__(self, conn):
self.conn = conn
def __enter__(self):
self.conn.execute("BEGIN")
return self.conn
def __exit__(self, exc_type, exc, tb):
if exc_type is None:
self.conn.execute("COMMIT")
else:
self.conn.execute("ROLLBACK")
return False # ne pas avaler l'exceptionLa version courte avec contextlib :
from contextlib import contextmanager
@contextmanager
def dossier_courant(chemin):
import os
ancien = os.getcwd()
os.chdir(chemin)
try:
yield
finally:
os.chdir(ancien)
with dossier_courant("/tmp"):
...Tout ce qui précède yield correspond à __enter__, ce qui suit à __exit__.
contextlib offre aussi suppress et ExitStack :
from contextlib import suppress, ExitStack
with suppress(FileNotFoundError):
Path("cache.tmp").unlink()
with ExitStack() as stack:
fichiers = [stack.enter_context(open(p)) for p in chemins]import time
from functools import wraps
def cache_ttl(secondes: float):
def decorateur(func):
memo: dict = {}
@wraps(func)
def wrapper(*args):
maintenant = time.monotonic()
if args in memo:
valeur, expire = memo[args]
if maintenant < expire:
return valeur
valeur = func(*args)
memo[args] = (valeur, maintenant + secondes)
return valeur
wrapper.vider = memo.clear
return wrapper
return decorateur
@cache_ttl(60)
def taux_change(devise: str) -> float:
...Générateurs pour les flux, décorateurs pour les comportements transversaux, context managers pour les ressources. Une fois ces trois outils intégrés, vous lirez le code des grands frameworks Python sans buter à chaque ligne.