Génie Python : Outils Fonctionnels & Astuces

Cours 5 — Le couteau suisse du code élégant
⚡ map · filter · reduce · zip · yield · *args · sorted · any/all

§1 Le Python qu'on utilise sans le connaître

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.

Les outils de ce cours sont comme les couteaux suisses : vous pouvez survivre sans, mais une fois que vous les avez, vous vous demandez comment vous faisiez avant. Et vous arrêtez de râler sur les autres langages qui n'ont pas zip. (Bon, vous râlerez quand même un peu.)
Le style fonctionnel vient du lambda-calcul d'Alonzo Church (vu au cours 1). Les langages fonctionnels Lisp (1958), ML (1973) et Haskell (1990) ont popularisé 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.

§2 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
25
7
négatif
Les lambdas sont limitiées à une expression (pas de boucle, pas de return explicite). Si votre lambda dépasse une ligne, utilisez une vraie fonction. Le code doit être lisible, pas un concours d'obfuscation.
La lambda est comme un couteau suisse trop petit : pratique pour les petits trucs, mais n'essayez pas de couper un arbre avec. Pour les trucs sérieux, sortez une vraie fonction.

§3 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)
[32.0, 50.0, 68.0, 86.0, 104.0]
[32.0, 50.0, 68.0, 86.0, 104.0]
[32.0, 50.0, 68.0, 86.0, 104.0]

3.1 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
[5, 7, 9]
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.

§4 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]]
[12, 18, 15, 10]
['Python', 'learning']
[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.)

Variation : filter + map = pipeline

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]
[144, 324, 225]
[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.


§5 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)
10
24
9
0
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.)
En Python, préférez 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.

§6 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)
[('Alice', 14), ('Bob', 8), ('Charlie', 16)]
Alice a 14
Bob a 8
Charlie a 16
('Alice', 'Bob', 'Charlie')
(14, 8, 16)

6.1 Transposition de matrice avec 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
[(1, 4), (2, 5), (3, 6)]
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.)

6.2 Créer un dictionnaire avec 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
{'a': 1, 'b': 2, 'c': 3}
{'python': 0, 'ia': 1, 'data': 2, 'apprentissage': 3}
{'python': 0, 'ia': 1, 'data': 2, 'apprentissage': 3}

§7 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
True
False
False
False
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.

§8 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']
['IA', 'data', 'Python', 'algorithme', 'apprentissage']
Charlie 16
Alice 14
Bob 8
['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.

§9 Générateurs et yield — la mémoire économique

Un 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
0 1 2 3 4

9.1 Pourquoi les générateurs changent la donne

# 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...
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 46368 75025 121393 196418 317811 514229 832040 1346269 ...
Un générateur est paresseux : il ne calcule que la valeur suivante quand on la demande. C'est parfait pour :
— Lire un fichier de 100 Go ligne par ligne (sans le charger en mémoire)
— Générer des nombres aléatoires en continu
— Parcourir des séquences infinies (Fibonacci, nombres premiers)
— Créer des pipelines de traitement de données à la carte
Les générateurs Python s'inspirent des ICL (Inter-Process Communication) d'Unix et des streams de Haskell. Mais leur véritable ancêtre est le langage Icon (1977), qui avait des « générateurs » capables de produire des séquences de valeurs et de reprendre leur exécution. Python a rendu cette idée accessible avec le mot-clé yield, bien plus simple que les coroutines d'autres langages.

9.2 Compréhensions génératrices (generator expressions)

# 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)
at 0x...>
285
332833500
Un générateur s'épuise : une fois qu'on a itéré dessus, il est vide. On ne peut pas le réutiliser. Si vous avez besoin de passer deux fois dessus, mettez-le dans une liste ou créez-en un nouveau.

§10 *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
6
30
0
nom = Python
age = 34
type = langage

10.1 L'opérateur étoile — dépaqueter (unpack)

# * 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
1 2 3
Couleur : bleu / Taille : 42
L'étoile * 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.)

§11 Applications concrètes pour l'IA

11.1 Normalisation de données en une ligne

# 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)
[0.14285714285714285, 0.5714285714285714, 0.2857142857142857, 1.0, 0.0]

11.2 Produit scalaire avec zip

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

11.3 Lire un CSV et filtrer avec un pipeline fonctionnel

# 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)
Notes admis : [14, 16, 11]
Moyenne : 13.666666666666666
Pareil : [14, 16, 11]

§12 Pour aller plus loin

12.1 Le décorateur @ — 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))

12.2 functools.partial — geler des paramètres

from 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

12.3 Itérer avec 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)]

12.4 Déballage étendu (walrus operator :=)

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)

§13 Le mot de la fin

Ce cours vous a donné les outils pour écrire du Python concis, expressif et efficace. Vous avez vu :

OutilÀ retenir
mapAppliquer une fonction à toute une séquence
filterGarder les éléments qui satisfont un test
reduceRéduire une séquence à une valeur
zipAssembler plusieurs séquences
any/allTester des conditions sur toute une séquence
sorted(key=...)Trier intelligemment
yieldCréer des générateurs économes en mémoire
*args/**kwargsFonctions flexibles
Un dernier mot sur Grace Hopper (1906–1992), qu'on a citée au cours 1. Elle a popularisé l'idée que les langages de programmation devaient ressembler à l'anglais plutôt qu'à du binaire. Elle a créé le premier compilateur (A-0, 1952) et le langage COBOL. Mais surtout, elle disait : « The most dangerous phrase in the language is: We've always done it this way. »

Les outils de ce cours (map, filter, zip, yield, *args) sont là pour vous empêcher de toujours faire les choses de la même façon. Explorez. Essayez. Certains deviendront vos réflexes. D'autres non. Mais au moins, vous les connaîtrez.
Bilan du cours 5 : Avec ce bagage, vous lisez le code Python comme un roman. Ou au moins comme une BD. Mais une BD avec beaucoup de lambda.
© 2026 — laurent.thiry@uha.fr