map()
, filter()
et reduce()
sont des fonctions de traitement d’itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce()
sera déplacée dans le module functools
pour Python 3.
Les opérations que font ces fonctions sont typiquement quelque chose que l’ont peut faire sans elles, et nous allons les passer en revue pour voir dans quels cas elles sont pertinentes, dans quel cas une alternative est meilleure. L’alternative étant, dans 90% des cas, une liste en intention.
filter()
filter()
prend une fonction en paramètre, souvent une lambda, comme ses deux soeurs puisqu’on est dans le paradigme fonctionnel. Elle doit renvoyer True
si on garde un élément, et False
sinon.
L’usage typique est celui-ci :
ages = range(30) majeurs = filter(lambda x: x > 18, ages) print(majeurs) ## [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] |
Typiquement, ce code peut être remplacé par une liste en intention dans le pur style Python :
majeurs = [a for a in ages if a > 18] |
Le code est plus court, et fait usage d’une des fonctionalités les plus importantes du langage. On peut en plus ajouter une transformation à a
facilement si on le désire, au lieu de devoir coller un map()
derrière.
filter()
est vraiment la moins utile des 3, et sera une question de style, surtout pour les nostalgiques de Lisp. Je répète souvent que quand on code avec un autre langage, on doit essayer de se tenir au style du nouveau et pas faire un mix avec ses anciennes habitudes. Quand je code en Java, je fais des getter et setter, même si j’ai horreur de ça.
Il existe quand même UNE astuce pour laquelle filter est utile : garder uniquement les éléments vrais d’une liste.
Typiquement :
l = [1, 0, None, [], True] print filter(bool, l) [1, True] |
Ca marche si la fonction pre-existe et qu’on a pas à faire une lambda, mais c’est vraiment le seul usage potable. Un peu plus court qu’une liste en intention.
map()
Si filter()
est l’équivalent de la partie de droite d’une liste en intention, map()
est l’équivalent de la partie de gauche. La fonction passée retourne un résultat qui permet de transformer la liste.
Typiquement :
memes = ["It's over 9000 !", "All your base are belong to us."] print(map(unicode.upper, memes)) |
Ce qui peut se traduire par :
print(s.upper() for s in memes) |
map()
est un peu plus utile, dans le sens où sa syntaxe peut être plus concise dans certains cas, comme le casting de types. Par exemple si je reçois une heure sous forme de string :
h, m, s = map(int, '8:19:22'.split(':')) |
sera plus court et plus concis, et plus clair que :
h, m, s = (int(i) for i in '8:19:22'.split(':')) |
Mais bon, la différence n’est pas non plus incroyable au point d’en faire une fonctionnalitéé clé. Je l’utilise de temps à autre par soucis de brièveté, mais vraiment c’est tout.
reduce()
reduce()
est plus tordu. La fonction doit prendre deux paramètres en entrée, et retourner une valeur. Au premier appel, les deux premiers éléments de l’itérable sont passés en paramètres. Ensuite, le résultat de cet appel et l’élément suivant sont passés en paramètre, et ainsi de suite.
Vous n’avez rien pigé ? C’est normal. reduce()
est parfaitement cryptique. Voici ce que ça donne en pratique :
def afficher(a, b): print("Entrée :", a, b) print("Sortie :", a + b) return a + b res = reduce(afficher, range(10)) print("Résultat final", res) ## Entrée : 0 1 ## Sortie : 1 ## Entrée : 1 2 ## Sortie : 3 ## Entrée : 3 3 ## Sortie : 6 ## Entrée : 6 4 ## Sortie : 10 ## Entrée : 10 5 ## Sortie : 15 ## Entrée : 15 6 ## Sortie : 21 ## Entrée : 21 7 ## Sortie : 28 ## Entrée : 28 8 ## Sortie : 36 ## Entrée : 36 9 ## Sortie : 45 ## Résultat final 45 |
Vous allez me dire, à quoi ça sert ? Et bien par exemple à appliquer des opérateurs commutatifs, ici nous l’avons fait avec +
, nous avons fait la somme de tous les éléments retournés par range(10)
. La preuve :
print(sum(range(10))) ## 45 |
Il n’y a pas, en Python, de fonction équivalent à sum()
pour la multiplication. Donc on ferait :
print(reduce(lambda a, b: a * b, range(1, 11))) ## 3628800 |
Ce qui multiplie tous les éléments entre eux. Comme l’ordre dans lequel les éléments sont multipliés n’a pas d’important (d’où le ‘commutatif’), ça fonctionne.
reduce()
peut prendre un troisième paramètre, initial
, qui sera la valeur passée en premier au premier appel de la fonction. Cela permet de travailler sur des calculs en cascade qui ne fonctionneraient sinon pas. Revenons à notre exemple de temps :
temps = map(int, '8:19:22'.split(':')) print(reduce(lambda a, b: a * 60 + b, temps, 0)) ## 29962 |
Ce qui peut se traduire par :
h, m, s = map(int, '8:19:22'.split(':')) print(h * 3600 + m * 60 + s) ## 29962 |
Bien sûr, cette conversion ne fonctionnerait pas si le calcul était sur un itérable plus long. Mais une version itérative est facile à faire :
res = 0 for i in map(int, '8:19:22'.split(':')): res = res * 60 + i print(res) ## 29962 |
Maintenant, autant les deux dernières versions sont faciles à comprendre, autant la première prend quelques secondes. Et c’est la raison pour laquelle reduce()
a été retirée des builtins, pour encourager l’usage des alternatives. En effet, cette fonction donne toujours un résultat très peu lisible. Je cite et approuve Guido là dessus:
C’est en fait celle que je déteste le plus, car, à part pour quelques exemples impliquant + ou *, presque chaque fois que je vois un appel à reduce() avec une fonction non-triviale passée en argument, j’ai besoin de prendre un crayon et un papier pour faire le diagramme de ce qui est effectivement entrée dans la fonction avant que je comprenne ce qu’est supposé faire reduce(). Donc à mes yeux, l’application de reduce() est plutôt limitée à des opérateurs associatifs, et dans d’autres cas il est mieux d’écrire une boucle d’accumulation explicitement.
Graissage maison.
Bref, reduce()
est dur à lire, et une boucle ne l’est pas. Écrivez 3 lignes de plus, ça ne va pas vous tuer. Relire votre one-liner dans un mois par contre…
Cette fonction a été beaucoup utilisée avec les opérateurs or
et and
pour savoir si tous les éléments étaient vrais au moins un élément vrai dans une liste :
tout_est_vrai = [1, 1, 1, 1] certains_sont_vrais = [1, 0, 1, 0] tout_est_faux = [0, 0, 0, 0] # Retourne True si tout est vrai print(bool(reduce(lambda a, b: a and b, tout_est_vrai))) ## True print(bool(reduce(lambda a, b: a and b, certains_sont_vrais))) ## False print(bool(reduce(lambda a, b: a and b, tout_est_faux))) ## False # Retourne True si au moins un élément est vrai print(bool(reduce(lambda a, b: a or b, tout_est_vrai))) ## True print(bool(reduce(lambda a, b: a or b, certains_sont_vrais))) ## True print(bool(reduce(lambda a, b: a or b, tout_est_faux))) ## False |
Mais aujourd’hui, c’est parfaitement inutile puisque nous avons les fonctions built-in all() et any(), qui font ça en plus court et plus rapide :
# Retourne True si tout est vrai print(all(tout_est_vrai)) ## True print(all(certains_sont_vrais)) ## False print(all(tout_est_faux)) ## False # Retourne True si au moins un élément est vrai print(any(tout_est_vrai)) ## True print(any(certains_sont_vrais)) ## True print(any(tout_est_faux)) ## False |
Petite astuce finale
Souvenez-vous également que les fonctions Python peuvent être déclarées n’importe où à la volée, même dans une autre fonction, une classe, une méthode, un context manager, etc. Or une fonction peut retourner un générateur grâce à yield, ce qui vous permet de déclarer des gros bouts de logique, et de les plugger dans votre process itérative a posteriori :
def traitement_complexe(iterable): for x in iterable: if x not in (1, 3, 7) and x % 2 != 0: if x + x < 13 : yield x else: yield x - 2 print("-".join(map(str, traitement_complexe(range(20))))) ## 5-7-9-11-13-15-17 |
j’ai déjà utilisé map() et filter() c’est vrais que c’est tordu mais je trouve surtout que ce n’est pas très lisible chaque fois que je tombe dessus je me dis “merde comment ça marche encore” (oui je suis pas dev a plein temps et mon niveau et tout petit)
Bonjour,
Je vois un cas d’utilisation de “map” intéressant, mais j’ai peut-être tord.
Dans mon code j’ai parfois besoin de faire des appels multiples à des fonctions, et ceux en me foutant éperdument de la liste résultante, seule l’exécution de la fonction m’intéresse.
Voici alors ce que je peux écrire:
[ fonction_del(id) for id in ids ]
Ou bien:
map(fonction_del, ids)
Dans la mesure ou je ne consulterai jamais le contenu de la liste que je génère dans mon premier code, je pense qu’il est mieux de faire du map et de ne pas créer un objet liste.(C’est un appel API avec levée d’exception si besoin).
Je suis dans le vrai Max ?
PS: bien entendu, j’aurai pu faire un boucle mais ça prend 2 lignes :D
Sauf qu’en python3, map retourne un itérateur, donc pour exécuter la fonction tu dois créer la liste de la même manière. cf http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html#map
Sinon pour ton problème une bête boucle for suffit (même si ça prend une ligne de plus…)
@Drife: oui. Sauf pour le nom. Moi c’est sam :) Mais comme le dit kontre, ça ne marchera plus en Python 3. Tu peux compenser en appelant all() sur le generateur, mais franchement, autant faire une boucle. L’intention est claire.
@Sam: Désolé pour le nom :).
Bon de toute façon cette écriture sera obsolète bientôt, mais en fait je crois que j’ai tord, car d’après la doc (cf kontre):
“much the same way as filter(), the map() function now returns an iterator. (In Python 2, it returned a list.)”
Donc, a priori la liste était générée de toute façon en python 2, tout comme avec une liste compréhension.
J’imaginai que map() se contentait d’itérer sans créer d’objet résultant…
Merci de vos réponses en tout cas ;-)
Une utilisation de map intéressante: le zip-long:
map(None, range(5), range(10))
Plus un détournement dirons-nous. Et comme “explicit is better than implicit”, autant utiliser l’outil officiel pour ça:
Bonjour bonjour,
Je ne comprends pas trop L’usage utile de filter (garder uniquement les elements vrais d’une liste).
Pourquoi ne pas faire :
[a for a in l if a]
C’est juste plus court.
Bonjour,
Juste un petit mot concernant l’optimisation.
L’article n’en parle pas du tout et ce n’est clairement pas l’objectif ici, mais il est bon de préciser que ces fonctions fonctionnent bien plus vite que leurs homologues en boucle for (Je ne parle pas des listes en intention, je n’en sais rien ;)
C’est ce qui leur donne (à mes yeux) leur seul intérêt d’ailleurs ^^
Un exemple ici : Python Patterns – An Optimization Anecdote
Et ici : PythonSpeed/PerformanceTips
J’arrive à trouver assez facilement des exemples naturels de cas où map sera plus efficace qu’une compréhension de liste (par exemple le map(str, …) illustré ci-dessus), mais j’ai beaucoup de mal à trouver des cas où filter sera plus recommandable en pratique (si on se focalise sur le temps d’exécution); il semble que les compréhensions de listes l’emportent toujours.
Ce qui me surprend fort dans la mesure où le fonctionnement de map et de filter devrait être assez similaire: ces fonctions ne font bien sûr pas la même chose, mais elles nous permettent de générer les objets à la volée plutôt que de stocker un itérable. Non?
Dès lors, l’usage de filter semble se restreindre aux cas où elle rend le code plus lisible (question de goût semble-t-il) ou à ceux où on doit vraiment faire gaffe à la consommation en mémoire.
Les comprehensions sont de la syntaxe, donc le code derrière map directement sur des structures C. map et filter sont des fonctions, elles prennent en paramètres une autre fonction, qui à chaque appel prend un paramètre à nouveau. Cela fait pas mal de référence python à résoudre à chaque tour de boucle, et c’est ce qui ralenti le process.
Alors je me trompe peut-être… mais un intérêt (et non des moindres) de map n’est-il pas de limiter les effets de bord ? Et ainsi d’avoir un code plus performant ?
Cela permet de faire des pipelines d’opération et d’affecter une seule fois une variable non ? Un exemple bête :
toto = un dataframe
titi = toto[‘une colonne’].map(fonction1)\
.map(fonction2)\
.map(fonction3)\
.map(fonction4), etc.
C’est beaucoup moins poussé que l’API Stream de Java8 (don je suis jaloux et qui manque sous Python ^^). Mais je trouve ça claire comme écriture… non ?
Ce ne serait pas le map, filter et reduce du builtin qui sont un peu vieillotte ? Python n’aurait-il pas à y gagner de s’inspirer de l’API Stream de Java8 ? (je suis pas un pro programmeur, ce sont de vrais questions)
C’est ce que font les listes en intension, mais avec une syntaxe native;
Salut !
Est-il possible d’utiliser map en mode “thread” vu que généralement, il ne fait pas d’effet de bord (ne modifie pas de valeurs) ?
Exemple:
from time import sleep
import super_map_thread
def long_calcul(x):
sleep(5)
return x + 1
liste = [i for i in range(20)]
map(long_calcul, liste) # très rapide, mais retourne un object map
list(map(long_calcul, liste)) # -> 5 sec * 20 = 100 sec
list(super_map_thread(long_calcul, liste)) # -> 5 sec (et quelques) en tout
Je sais qu’on peut le faire avec des threads. Je me demande si il y a pas un module qui fait ça ? Ou alors l’utilisation n’est pas assez important (ou tout simplement que ce n’est pas fait pour ?) pour que quelqu’un ait crée un module ?
Google “python thread pool” ou “python multiprocessing pool”. Les pools ont des méthodes map() qui distribuent le travail à des workers.
juste un petit message pour ceux qui s’interrogent sur l’intérêt de map et filter…
Ces fonctions typiques de la programmation fonctionnelle sont par exemple très fortement utilisées dans des bibliothèques comme py-linq https://github.com/viralogic/py-enumerable ou asq https://github.com/sixty-north/asq qui permettent d’avoir une syntaxe à la LINQ (Language-Integrated Query) pour faire des requêtes (avec une API assez sympa… mais c’est une question de goût) sur des itérables.
Salut,
Je vois également un autre intérêt aux map, filter et deduce et de celui de chainer des opérations sur des itérables sans consommer un mémoire. Par exemple je peux faire :
a = map(lambda x: x**2, range(10000))
b = filter(lambda x: x%2 == 0, a)
c = map(lambda x: x/3, b)
…
z = reduce(lambda a, b: a/b, y, 12)
print(z)
Finalement la fonction terminale fera qu’une seule passe sur l’itérable du début ne coûtant ainsi rien en espace RAM et avec d’excellentes performances… Çà peut être très utile si on manipule d’énormes variables par rapport à l’espace RAM, ce qui est aujourd’hui souvent le cas.
Mais je me trompe peut-être, j’aimerais l’avis de SAM…
Les expressions génératrices font pareille.
Oui il est vrai qu’on pourrait faire un truc du genre :
a = (ii**2 for ii in range(1000))
b = (ii for ii in a if ii%2==0)
c = (ii/3 for ii in b)
...
y = ...
z = reduce(lambda a, b: a/b, y, 12)
Alors là c’est peut-être personnel… mais je trouve que l’écriture avec les map dans ce cas est plus facilement lisible… nope ?
Surtout que je pense que derrière le code doit être sensiblement identique, car au final le map dans python 3 est un simple mapage sur un yield, nope ?
Enfin, dernier argument que je trouve en faveur des mot clef map, filter et reduce, c’est d’être raccord avec un certain nombre d’autres langages (Java 8/9 avec l’API Stream, Scala, Spark, etc.) et ainsi peut-être de faciliter la transition avec ces langages et Python (et vice-versa) ;-)
Reduce est toujours dur à comprendre et à débugger. Et les intensions sous toujours plus rapides à exécuter car on évide un appel de fonction. Après si tu as déjà la fonction déclarée, un map est plus direct.