Skip to content

Tout pour WordPress, le développement web — et plus encore

🐍 Programmation fonctionnelle en Python

🐍 Programmation fonctionnelle en Python

Tout développeur Python finit par entendre: «essayez d’écrire dans un style fonctionnel, votre code sera plus propre.» Mais quand vous ouvrez la documentation, vous voyez map, filter et reduce avec des lambdas et vous ne savez pas par où commencer.

Le problème n’est pas que la programmation fonctionnelle est difficile. Le problème est que la plupart des guides plongent dans les fourrés académiques de Haskell ou se limitent à la syntaxe sans expliquer le «pourquoi». Python, en tant que langage multi-paradigme, fournit exactement les outils fonctionnels dont vous avez besoin pour les tâches quotidiennes, sans extrêmes.

Dans ce document, une analyse pratique des capacités fonctionnelles de Python: des compréhensions de listes aux fonctions lambda, avec des exemples concrets et des scénarios où chaque outil simplifie réellement le code.

💡 Aperçu rapide:

  • Décortiquer les compréhensions de listes, pourquoi elles sont meilleures que les boucles et quand les utiliser
  • Maîtriser les générateurs: yield, économies de mémoire et expressions génératrices
  • Parcourir map + filter + reduce: exemples pratiques de remplacement de boucles
  • Apprendre à écrire des fonctions lambda et comprendre où elles sont appropriées et où elles nuisent à la lisibilité

Programmation fonctionnelle, de quoi parle-t-on

La programmation fonctionnelle est une approche où un programme est construit à partir de fonctions mathématiques: elles prennent des données d’entrée immuables et retournent un résultat sans rien modifier à l’extérieur. Pas d’effets de bord, pas de modification d’état global.

Dans les langages fonctionnels purs comme Haskell, les parties «pures» et «impures» (qui interagissent avec le monde extérieur) du programme sont strictement séparées. Cette approche permet une preuve formelle de la correction du code: le compilateur garantit l’absence d’effets de bord inattendus.

Python n’a pas été conçu comme un langage fonctionnel. Il est multi-paradigme: orienté objet, procédural et, oui, fonctionnel aussi. Écrire en Python dans un style purement fonctionnel, c’est comme enfoncer des clous avec un microscope: techniquement possible, mais inconfortable et peu naturel. En revanche, prendre des outils fonctionnels individuels et les intégrer dans du code familier est un schéma qui fonctionne et qui est utile.

Compréhensions de listes, le principal outil fonctionnel de Python

Le pain quotidien de la programmation fonctionnelle, c’est le travail sur les listes. Sélectionner des éléments selon une condition, transformer chaque élément, construire une nouvelle liste à partir d’une existante, tout cela est couvert en Python par les compréhensions de listes.

Avant, boucle procédurale:

1def filter_odd(li):
2 result = []
3 for i in li:
4 if i % 2 == 1:
5 result.append(i)
6 return result
7
8print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))

Après, compréhension de liste:

1li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31]
2odd_numbers = [x for x in li if x % 2 == 1]
3print(odd_numbers) # [7, 1, 19, 31]

Quatre lignes de code procédural compressées en une seule. La lisibilité n’en a pas souffert: la syntaxe [expression for element in iterator if condition] est intuitivement claire, «prendre x dans li si x est impair».

Et ce n’est pas seulement du sucre syntaxique. La compréhension de liste s’exécute plus vite qu’une boucle équivalente avec .append() parce qu’elle s’exécute au niveau C, pas au niveau de l’interpréteur Python.

Qu’est-ce qu’un itérateur

La compréhension de liste repose sur le concept d’itérateur, un objet qui retourne l’élément suivant d’une séquence quand next() est demandé. Tout objet implémentant la méthode __iter__ est appelé itérable. Liste, chaîne, tuple, dictionnaire, ensemble, ils sont tous itérables.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(result) # FOOBAR

Ici iter() retourne un itérateur de chaîne, et la compréhension de liste le parcourt en appliquant .upper() à chaque caractère.

Générateurs, séquences paresseuses

Un itérateur peut être créé non seulement à partir d’une collection existante, mais aussi à l’aide d’une fonction génératrice. Au lieu de return, elle utilise yield: la fonction «s’endort», en mémorisant son état, et au prochain appel next() reprend au même endroit.

1def gen(max_val):
2 i = 1
3 while i < max_val:
4 yield i
5 i += 1
6
7g = gen(1000)
8for _ in range(12):
9 print(next(g))

Ce code affichera les nombres de 1 à 12. La fonction gen ne crée pas une liste de mille éléments en mémoire, elle génère les valeurs une par une, à la demande. Pour des séquences de millions d’enregistrements (lignes d’un fichier de log, flux de base de données), la différence de consommation mémoire est de plusieurs ordres de grandeur.

Expressions génératrices

Syntaxe compacte: la même chose que les compréhensions de listes, mais avec des parenthèses au lieu de crochets.

1g = (c.upper() for c in "foobar")

Ce n’est pas un tuple, c’est un générateur. La règle est simple: crochets → liste (eager, tout en mémoire), parenthèses → générateur (paresseux, un élément à la fois).

Les générateurs sont un pont entre le style procédural et le style fonctionnel en Python: ils fournissent une évaluation paresseuse sans plonger dans la théorie des monades.

Map, filter et reduce: trois piliers du traitement fonctionnel

Trois fonctions natives qui, dans les langages fonctionnels, sont le fondement de tout. Python les implémente à sa manière, et il est important de connaître les nuances des versions spécifiques à Python.

Map, appliquer une fonction à chaque élément

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(list(result)) # [1, 4, 9]

map prend une fonction et une séquence, et retourne un itérateur avec les résultats de l’application de la fonction à chaque élément. En Python 3, map retourne un itérateur, pas une liste, donc vous avez besoin de list() pour voir le résultat.

Pour les transformations simples, la compréhension de liste est souvent plus lisible:

1[x * x for x in [1, 2, 3]] # same thing, but more familiar

Filter, sélectionner les éléments selon une condition

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(list(result)) # ['F', 'O', 'O']

filter conserve uniquement les éléments pour lesquels la fonction prédicat retourne True. Comme map, il retourne un itérateur en Python 3.

Reduce, replier une séquence en une seule valeur

Contrairement à map et filter, reduce ne réside pas dans la portée native, mais dans le module functools. Il applique séquentiellement une fonction aux éléments, en accumulant le résultat.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(reduce(add, range(1, 6))) # 15

Sous le capot: ((((1 + 2) + 3) + 4) + 5) = 15. Pour sommer des nombres en Python, il y a la fonction native sum(), donc reduce est plus souvent utilisé pour des repliements non standard: construire un arbre à partir d’une liste plate, fusionner des dictionnaires imbriqués, calculer le plus grand diviseur commun d’une séquence.

Fonctions lambda, aides anonymes

Quand une fonction est nécessaire exactement une fois et que son corps tient en une expression, utilisez lambda:

1lambda arguments: expression

Le même exemple avec filter peut être réécrit sans def séparé:

1result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))

Lambda est un compromis. Avantage: pas besoin de déclarer une fonction séparée pour une vérification triviale. Inconvénient: la lisibilité chute si l’expression devient plus complexe que quelques opérations. Règle empirique: si la lambda ne tient pas sur une ligne ou si vous commencez à imbriquer des lambdas les unes dans les autres, extrayez un def classique.

Où les lambdas sont réellement utiles

Le scénario le plus courant est le tri par une clé non standard:

1users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}]
2users.sort(key=lambda u: u["age"])

Ou une transformation rapide dans map/filter quand le corps est vraiment trivial:

1squares = list(map(lambda x: x * x, range(10)))

Mais si vous vous apprêtez à utiliser une lambda pour une logique de quatre ou cinq opérations, arrêtez-vous. Un nom de fonction sert de documentation, et def offre de l’espace pour une docstring.

⁉️🤔 Questions fréquentes

Quand une compréhension de liste est-elle meilleure que map?

Une compréhension de liste comme [x*2 for x in data] se lit plus naturellement que list(map(lambda x: x*2, data)). Une opération native avec une lambda a deux niveaux d’indirection, alors qu’une compréhension de liste en a un. Mais si la fonction existe déjà en tant que def séparé, map avec elle est concis. La compréhension de liste l’emporte en lisibilité pour les transformations simples, surtout avec un filtrage via [... if ...]. Pour une logique complexe avec des variables intermédiaires, une boucle for classique est imbattable.

Générateur ou liste, lequel choisir?

Si le résultat est nécessaire plusieurs fois (itération, indexation, longueur), prenez une liste. Si vous itérez une seule fois et que le volume de données est important, un générateur économisera de la mémoire. Une liste d’un million d’entiers occupe environ 8 Mo, un générateur pour la même plage occupe moins d’un kilo-octet. Mais un générateur ne peut pas être «rembobiné» ni accédé par index. Règle pratique: données provenant d’un fichier ou d’une base de données, générateur; résultat pour une utilisation répétée, list().

Les lambdas nuisent-elles à la lisibilité du code?

À petites doses et pour une logique triviale, non. key=lambda x: x["price"] se lit instantanément. Mais dès qu’une lambda cesse d’être «une seule pensée», extrayez-la dans une fonction classique. Les équipes avec revue de code limitent les lambdas à une expression sans imbrication. Si un collègue a besoin de plus de cinq secondes pour la comprendre, réécrivez-la en def. Le nom sert de documentation: def by_price(item): return item["price"] est plus clair qu’une lambda anonyme.

Le Python fonctionnel est-il beaucoup plus lent que le procédural?

Pour la plupart des tâches quotidiennes, la différence est négligeable, de l’ordre de fractions de microseconde. La compréhension de liste est souvent plus rapide qu’une boucle équivalente parce qu’elle est implémentée en C. Le goulot d’étranglement dans les programmes Python n’est pas le choix entre map et une boucle, mais les entrées/sorties, les requêtes réseau et l’architecture en général. Écrivez clairement, profilez les points chauds avec cProfile, et n’optimisez que ce qui ralentit réellement les choses.

Ce qu’il faut utiliser dans votre code dès aujourd’hui

Les outils fonctionnels de Python ne sont pas un dogme, mais des leviers. Ils ne remplacent pas le code procédural et orienté objet, mais le complètent à des endroits précis.

Commencez par les compréhensions de listes, c’est l’application la plus courante et la plus sûre. Maîtrisez les générateurs: dès que vous voyez une liste temporaire qui n’est utilisée qu’une fois, demandez-vous si cela ne devrait pas être remplacé par yield. Utilisez map et filter là où ils simplifient réellement une chaîne de transformation. Réservez les lambdas pour key= dans les tris et les callbacks triviaux.

Le style fonctionnel en Python ne concerne pas la «pureté», mais l’expressivité. Si après avoir remplacé une boucle par une compréhension de liste le code est devenu plus clair, vous avez tout fait correctement. Essayez de réécrire une méthode problématique dans une veine fonctionnelle et comparez le diff: souvent, le résultat parle de lui-même.