Après quatre cours, vous maîtrisez les fondamentaux : listes, dictionnaires, boucles, fonctions. Mais Python cache une boîte à outils qui rend le code plus court, plus lisible, et souvent plus rapide. C'est ce qu'on appelle le style fonctionnel.
zip. (Bon, vous râlerez quand
même un peu.)
map, filter et reduce.
Python les a adoptés — puis a ajouté les compréhensions de listes,
plus pythoniques. Aujourd'hui, on utilise les deux.
lambda — la fonction sans nom (reloaded)
On a vu lambda au cours 1. Mais il est temps de
l'utiliser partout, car c'est le carburant de
tous les outils qui suivent.
# Une lambda, c'est une fonction sans nom, en une ligne
carré = lambda x: x ** 2
print(carré(5)) # 25
# Lambda à plusieurs paramètres
addition = lambda a, b: a + b
print(addition(3, 4)) # 7
# Lambda avec condition (opérateur ternaire)
signe = lambda n: "positif" if n >= 0 else "négatif"
print(signe(-5)) # négatif
return explicite). Si votre lambda
dépasse une ligne, utilisez une vraie fonction. Le code doit
être lisible, pas un concours d'obfuscation.
map — appliquer à toute la liste
map(f, iterable) applique la fonction f à chaque
élément de l'itérable et retourne un itérateur. C'est une
boucle for implicite.
def celsius_vers_fahrenheit(c):
return c * 1.8 + 32
temperatures_c = [0, 10, 20, 30, 40]
# Version boucle
temperatures_f = []
for t in temperatures_c:
temperatures_f.append(celsius_vers_fahrenheit(t))
# Version map
temp_f_map = list(map(celsius_vers_fahrenheit, temperatures_c))
# Version map + lambda (quand la fonction est trop simple pour un nom)
temp_f_lambda = list(map(lambda c: c * 1.8 + 32, temperatures_c))
print(temperatures_f)
print(temp_f_map)
print(temp_f_lambda)
map avec plusieurs itérables# Additionner deux listes élément par élément (comme des vecteurs)
a = [1, 2, 3]
b = [4, 5, 6]
somme = list(map(lambda x, y: x + y, a, b))
print(somme) # [5, 7, 9]
# Produit scalaire avec map et sum
produit_scalaire = sum(map(lambda x, y: x * y, a, b))
print(produit_scalaire) # 32
map est un héritage direct de Lisp (1958),
l'un des plus vieux langages encore utilisés. En Lisp, on écrit
(mapcar #'celsius-to-fahrenheit '(0 10 20)). En Python,
map(celsius_vers_fahrenheit, [0, 10, 20]). Même idée,
syntaxe plus claire. Lisp avait 30 ans d'avance sur son temps ;
Python l'a rendu accessible.
map() est paresseux : il ne calcule les
résultats qu'au fur et à mesure qu'on les demande. C'est pourquoi
on enveloppe souvent avec list() pour tout calculer
d'un coup. Si vous travaillez sur des données massives, la version
paresseuse économise de la mémoire.
filter — garder les bons
filter(f, iterable) garde uniquement les éléments pour
lesquels f renvoie True.
notes = [12, 5, 18, 7, 15, 3, 10]
# Garder les notes ≥ 10
adm = list(filter(lambda n: n >= 10, notes))
print(adm) # [12, 18, 15, 10]
# Garder les mots de plus de 5 lettres
mots = ["IA", "Python", "deep", "learning", "data"]
longs = list(filter(lambda m: len(m) > 4, mots))
print(longs) # ['Python', 'learning']
# filter sans fonction = garder les valeurs « truthy »
valeurs = [0, 1, "", "hello", None, [], [1, 2]]
propres = list(filter(None, valeurs))
print(propres) # [1, 'hello', [1, 2]]
filter, c'est le videur de boîte de nuit. Il regarde
chaque élément et dit : « toi tu rentres, toi tu dégages ».
Sauf que filter est 100% objectif : il ne juge que sur la fonction
que vous lui donnez. Pas de critères physiques. (Enfin, si,
vous pouvez filtrer par taille, mais c'est légal.)
On peut enchaîner : d'abord filtrer, puis transformer.
# Notes admis, au carré
notes = [12, 5, 18, 7, 15]
resultat = list(map(lambda n: n ** 2, filter(lambda n: n >= 10, notes)))
print(resultat) # [144, 324, 225]
# Avec une compréhension (souvent plus lisible)
resultat2 = [n ** 2 for n in notes if n >= 10]
print(resultat2) # [144, 324, 225]
Les deux versions sont équivalentes. En Python, on préfère souvent
les compréhensions pour leur lisibilité. Mais dans des pipelines
complexes, map et filter s'enchaînent
naturellement.
reduce — tout résumer en une valeur
reduce(f, iterable) applique cumulativement f pour réduire
l'itérable à une seule valeur. En Python 3, reduce a été
déplacée dans functools (elle fait moins peur comme ça).
from functools import reduce
# reduce(lambda a, b: a + b, [1, 2, 3, 4])
# = ((1 + 2) + 3) + 4 = 10
somme = reduce(lambda a, b: a + b, [1, 2, 3, 4])
print(somme) # 10
# Produit de tous les éléments
produit = reduce(lambda a, b: a * b, [1, 2, 3, 4])
print(produit) # 24
# Maximum
maximum = reduce(lambda a, b: a if a > b else b, [3, 7, 2, 9, 5])
print(maximum) # 9
# Avec valeur initiale (si l'itérable est vide)
total = reduce(lambda a, b: a + b, [], 0) # 0 (pas d'erreur)
print(total)
reduce est rendu célèbre par Google MapReduce
(Dean & Ghemawat, 2004), le framework qui a permis à Google
de traiter des pétaoctets de données. Le principe : on map
sur des données réparties sur des milliers de machines, puis on
reduce les résultats. Et voilà, vous savez comment
fonctionnent les大数据. (En vrai, c'est un peu plus compliqué,
mais pas tant que ça.)
sum(), max(), min(),
any(), all() à reduce. Réservez
reduce aux cas vraiment spécifiques (compositions de
fonctions, calculs personnalisés). Comme disait Guido van Rossum :
« reduce() is the most hateful thing in Python » —
enfin pas exactement, mais il a envisagé de le supprimer.
zip — la fermeture éclair
zip(*iterables) aggrège les éléments de plusieurs itérables
en tuples. Comme une fermeture éclair qui assemble les dents.
noms = ["Alice", "Bob", "Charlie"]
notes = [14, 8, 16]
# Zipper : (nom, note) pour chaque étudiant
couples = list(zip(noms, notes))
print(couples) # [('Alice', 14), ('Bob', 8), ('Charlie', 16)]
# Parcourir simultanément
for nom, note in zip(noms, notes):
print(nom, "a", note)
# Unzip : séparer un zip en listes
noms2, notes2 = zip(*couples)
print(noms2) # ('Alice', 'Bob', 'Charlie')
print(notes2) # (14, 8, 16)
zip# Souvenez-vous du cours 3 : on avait implémenté une transposition
# avec des boucles. Avec zip, c'est une ligne.
matrice = [
[1, 2, 3],
[4, 5, 6]
]
transposee = list(zip(*matrice))
print(transposee) # [(1, 4), (2, 5), (3, 6)]
# Chaque ligne de la transposée = colonne de l'originale
zip(*matrice) est l'incantation magique qui transpose
une matrice. Retenez-la : elle impressionne en entretien technique.
C'est le genre de ligne qui fait dire « ce candidat maîtrise Python
sur le bout des doigts ». (Ou alors « ce candidat a lu un blog
la veille ». Dans les deux cas, vous aurez le job.)
zip# zip + dict = dictionnaire clé/valeur
cles = ["a", "b", "c"]
valeurs = [1, 2, 3]
dico = dict(zip(cles, valeurs))
print(dico) # {'a': 1, 'b': 2, 'c': 3}
# Application : créer un vocabulaire mot→index pour le NLP
mots = ["python", "ia", "data", "apprentissage"]
vocab = {mot: i for i, mot in enumerate(mots)}
print(vocab) # {'python': 0, 'ia': 1, 'data': 2, 'apprentissage': 3}
# En une ligne avec zip et range
vocab2 = dict(zip(mots, range(len(mots))))
print(vocab2) # identique
any et all — des questions sur la liste
any(iterable) retourne True si au moins un
élément est vrai. all(iterable) retourne True
si tous le sont.
notes = [12, 5, 18, 7, 15]
# Quelqu'un a-t-il eu 18 ou plus ?
print(any(n >= 18 for n in notes)) # True
# Tout le monde a-t-il la moyenne (≥10) ?
print(all(n >= 10 for n in notes)) # False
# Quelqu'un a-t-il eu en dessous de 5 ?
print(any(n < 5 for n in notes)) # False
# Y a-t-il des doublons dans une liste ?
def a_doublons(liste):
return len(liste) != len(set(liste))
print(a_doublons([1, 2, 3])) # False
print(a_doublons([1, 2, 1])) # True
any et all sont paresseux :
any s'arrête dès qu'il trouve un True,
all s'arrête dès qu'il trouve un False.
C'est ce qu'on appelle l'évaluation court-circuit.
Pour des listes de millions d'éléments, ça change la vie.
sorted avec key — trier intelligemment
sorted(iterable, key=f) trie en utilisant f pour extraire
la clé de comparaison. C'est infiniment plus puissant que trier
directement.
# Trier des chaînes par longueur
mots = ["Python", "IA", "apprentissage", "data", "algorithme"]
tri_long = sorted(mots, key=lambda m: len(m))
print(tri_long) # ['IA', 'data', 'Python', 'algorithme', 'apprentissage']
# Trier des dictionnaires par une clé
etudiants = [
{"nom": "Alice", "note": 14},
{"nom": "Bob", "note": 8},
{"nom": "Charlie", "note": 16},
]
tri_notes = sorted(etudiants, key=lambda e: e["note"], reverse=True)
for e in tri_notes:
print(e["nom"], e["note"]) # Charlie 16, Alice 14, Bob 8
# Trier par plusieurs critères : note, puis nom
tri_multi = sorted(etudiants, key=lambda e: (-e["note"], e["nom"]))
# Trier par la dernière lettre d'un mot
mots = ["python", "java", "rust", "lisp"]
tri_derniere = sorted(mots, key=lambda m: m[-1])
print(tri_derniere) # ['java', 'lisp', 'python', 'rust']
key=lambda est l'incantation magique qui rend le tri
intelligent. Sans key, Python ne sait pas comment
comparer des dictionnaires, des objets, ou des chaînes selon
des critères bizarres (comme la dernière lettre). Avec
key, tout devient possible. C'est comme donner
une règle du jeu à Python avant le match.
yield — la mémoire économiqueUn générateur est une fonction qui produit des valeurs à la demande, une par une, au lieu de toutes les calculer d'un coup et de les stocker en mémoire.
# Une fonction normale retourne une liste (tout d'un coup)
def compte_normal(n):
resultat = []
for i in range(n):
resultat.append(i)
return resultat
# Un générateur produit les valeurs une par une (yield)
def compte_generateur(n):
for i in range(n):
yield i # « rends » une valeur mais garde l'état
# Les deux s'utilisent pareil dans une boucle for
for v in compte_generateur(5):
print(v, end=" ")
# Sortie : 0 1 2 3 4
# Avec un générateur, on peut traiter des données
# qui ne tiennent PAS en mémoire
def fib_generator(limite):
a, b = 0, 1
while a < limite:
yield a
a, b = b, a + b
# Calculer Fibonacci jusqu'à 10¹² sans stocker toute la suite
for f in fib_generator(10 ** 12):
print(f, end=" ")
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610...
yield, bien plus
simple que les coroutines d'autres langages.
# Une compréhension de liste crée toute la liste en mémoire
carres = [x ** 2 for x in range(10)] # 10 éléments
# Une expression génératrice utilise (...) au lieu de [...],
# et ne crée rien tant qu'on n'itère pas
carres_gen = (x ** 2 for x in range(10))
print(carres_gen) #
print(sum(carres_gen)) # 285 — calcule sans stocker
# Application : somme des carrés des nombres pairs jusqu'à 1000
s = sum(x ** 2 for x in range(1000) if x % 2 == 0)
print(s)
*args et **kwargs — le nombre variable de paramètres
Parfois, on ne sait pas à l'avance combien d'arguments une fonction
recevra. *args attrape les arguments positionnels dans
un tuple, **kwargs attrape les arguments nommés dans
un dictionnaire.
def somme_tout(*args):
"""Additionne un nombre variable de nombres."""
total = 0
for v in args:
total = total + v
return total
print(somme_tout(1, 2, 3)) # 6
print(somme_tout(10, 20)) # 30
print(somme_tout()) # 0
def presente(**kwargs):
"""Affiche des informations nommées."""
for cle, valeur in kwargs.items():
print(cle, "=", valeur)
presente(nom="Python", age=34, type="langage")
# nom = Python
# age = 34
# type = langage
# * dépaquette une liste en arguments positionnels
nombres = [1, 2, 3]
print(*nombres) # 1 2 3 — équivaut à print(1, 2, 3)
# ** dépaquette un dict en arguments nommés
config = {"couleur": "bleu", "taille": 42}
def affiche_config(couleur, taille):
print("Couleur :", couleur, "/ Taille :", taille)
affiche_config(**config) # Couleur : bleu / Taille : 42
* est le « ouvrir » des listes, et
** le « déplier » des dictionnaires. C'est magique
pour passer des paramètres dynamiquement. Utilisé intelligemment,
ça rend votre code flexible. Utilisé n'importe comment, ça rend
votre code illisible. Avec un grand pouvoir vient une grande
responsabilité. (Merci Tante May.)
# Normalisation min-max avec map et une lambda
donnees = [2, 5, 3, 8, 1]
min_v, max_v = min(donnees), max(donnees)
normalise = list(map(lambda x: (x - min_v) / (max_v - min_v), donnees))
print(normalise)
produit_scalaire = sum(a * b for a, b in zip(u, v))
# C'est la version la plus pythonique qu'on ait vue jusqu'ici
# Simulons un fichier CSV (lignes d'un vrai fichier)
lignes_csv = [
"nom,note,age",
"Alice,14,22",
"Bob,8,23",
"Charlie,16,21",
"Dana,11,24",
]
# Pipeline : découper → ignorer l'en-tête → extraire notes → filtrer → compter
en_tetes = lignes_csv[0].split(",")
donnees = list(map(lambda l: l.split(","), lignes_csv[1:]))
notes = list(map(lambda d: int(d[1]), donnees))
adm = list(filter(lambda n: n >= 10, notes))
moyenne = sum(adm) / len(adm)
print("Notes admis :", adm)
print("Moyenne :", moyenne)
# Version équivalente en compréhensions
notes_v2 = [int(l.split(",")[1]) for l in lignes_csv[1:]]
adm_v2 = [n for n in notes_v2 if n >= 10]
print("Pareil :", adm_v2)
@ — enrober une fonction
Un décorateur prend une fonction et en retourne une
autre (une fonction d'ordre supérieur, comme la dérivée numérique
du cours 1). La syntaxe @ est juste du sucre syntaxique.
def chronometre(f):
def wrapper(*args, **kwargs):
import time
debut = time.time()
resultat = f(*args, **kwargs)
fin = time.time()
print(f.__name__, "a pris", fin - debut, "secondes")
return resultat
return wrapper
@chronometre
def gros_calcul(n):
return sum(i ** 2 for i in range(n))
print(gros_calcul(10_000_000))
functools.partial — geler des paramètresfrom functools import partial
def puissance(base, exp):
return base ** exp
carre = partial(puissance, exp=2)
cube = partial(puissance, exp=3)
print(carre(5)) # 25
print(cube(3)) # 27
itertools
Le module itertools est le couteau suisse des itérateurs :
from itertools import chain, cycle, repeat, product, combinations
# chaîner des itérables
for x in chain([1, 2], ["a", "b"]):
print(x, end=" ") # 1 2 a b
# combinaisons (utile en stats)
comb = list(combinations([1, 2, 3], 2))
print(comb) # [(1, 2), (1, 3), (2, 3)]
:=)Python 3.8 a introduit l'opérateur morse (walrus) : il permet d'assigner une variable à l'intérieur d'une expression.
# Sans walrus : il faut appeler deux fois
if len(notes) > 0:
m = sum(notes) / len(notes)
print(m)
# Avec walrus : on assigne et on teste dans la même expression
if (n := len(notes)) > 0:
m = sum(notes) / n
print(m)
Ce cours vous a donné les outils pour écrire du Python concis, expressif et efficace. Vous avez vu :
| Outil | À retenir |
|---|---|
map | Appliquer une fonction à toute une séquence |
filter | Garder les éléments qui satisfont un test |
reduce | Réduire une séquence à une valeur |
zip | Assembler plusieurs séquences |
any/all | Tester des conditions sur toute une séquence |
sorted(key=...) | Trier intelligemment |
yield | Créer des générateurs économes en mémoire |
*args/**kwargs | Fonctions flexibles |
mappez comme un pro.filterez sans pitié.reducez vos problèmes à leur essence.zipez tout ce qui bouge.yieldez sans céder.*argsez sans limite.lambda.