map(), filter() et reduce () ?


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

22 thoughts on “map(), filter() et reduce () ?

  • 6no

    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)

  • Drife

    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

  • Sam Post author

    @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.

  • Drife

    @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 ;-)

  • Alcolo47

    Une utilisation de map intéressante: le zip-long:
    map(None, range(5), range(10))

  • Sam Post author

    Plus un détournement dirons-nous. Et comme “explicit is better than implicit”, autant utiliser l’outil officiel pour ça:

    from itertools import izip_longest
    zip_longest(range(5), range(10)))
  • gaut

    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]

  • Thomas974

    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

  • Sam Post author
       >>> %timeit list(map(str, range(10000)))
        1000 loops, best of 3: 1.73 ms per loop
     
        >>> %timeit [str(x) for x in range(10000)]
        100 loops, best of 3: 2.35 ms per loop
  • Bai4Faek

    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.

  • Sam Post author

    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.

  • Kirire

    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)

  • Sam Post author

    C’est ce que font les listes en intension, mais avec une syntaxe native;

  • Xuan

    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 ?

  • Sam Post author

    Google “python thread pool” ou “python multiprocessing pool”. Les pools ont des méthodes map() qui distribuent le travail à des workers.

  • Sébastien

    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.

  • Cyrile

    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…

  • Cyrile

    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) ;-)

  • Sam Post author

    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.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.