Notes d'apprentissage de Python : traitement des séquences avec un style fonctionnel - reduce
Contenu
Contexte
Ce billet s’inscrit dans une série de billets sur le traitement des séquences avec un style fonctionnel en Python.
NB : La version de Python utilisée dans les exemples de code est la version 3.
Principe
Comme filter et map, la fonction reduce
transforme une séquence qui lui est fourni en paramètre grâce à une fonction qui lui est également fournie en paramètre en parcourant la séquence de la gauche vers la droite.
A la différence de filter qui modifie la séquence originale en supprimant ses éléments qui sont faux pour un prédicat pour produire une nouvelle séquence ou à la différence de map qui transforme individuellement chaque élément de la séquence pour produire une nouvelle séquence, reduce
parcours la séquence de la gauche vers la droite en combinant les éléments afin de transformer la séquence elle-même en un nouvel objet (une valeur scalaire, une nouvelle séquence, etc.).
Nous allons éclairer tout cela par plusieurs exemples, mais avant il faut également noter qu’en Python contrairement à filter et à map, reduce
n’est pas (en fait n’est plus) une fonction native : elle se trouve dans le module functools.
Reductio ad exemplum
L’idée avec la fonction reduce
comme son nom le rappel est de réduire une séquence à un résultat.
Juste faire une réduction en somme !
L’exemple classique est la somme d’une liste de nombres : je souhaite réduire la liste de nombres à la somme de ces nombres.
Prenons la liste des nombres de 1 à 10 qui peuvent nous être donnés en Python avec range(1,11)
et calculons leur somme.
Commençons par le faire de manière impérative avec for
.
|
|
la variable sum_of_numbers_1
initialisée à 0
, sert d’acummulateur : dans la boucle on ajoute successivement chaque élément de la liste à cet accumulateur.
|
|
C’est très exactement ce que va faire reduce
:
|
|
La fonction reduce
prend en paramètre une fonction et un iterator. On notera que cette fonction passée à reduce
prend elle-même 2 paramètres en entrée. Elle produit une valeur en sortie qui n’est pas nécessairement du même type que celui des éléments en entrée (même si c’est le cas ici avec l’addition).
|
|
Que se passe-t-il la séquence en paramètre n’a qu’un seul élément ?
|
|
Et bien, cet élément est retourné.
|
|
Et que se passe-t-il avec la liste vide ?
|
|
Et bien dans ce cas on a une exception
|
|
Cela semble normal, il n’y a aucune valeur cohérente que reduce
puisse retourner dans ce cas.
En fait reduce
prend un troisième paramètre optionel qui correspond à une première valeur initiale pour démarrer la réduction. Avec ce troisième paramètre, on peut lui passer une séquence vide, c’est cette valeur initiale qui sera retournée.
|
|
|
|
Cela n’a pas à voir directement avec reduce
mais au passage on peut noter que dans le cas précis de l’addition que nous avons ici, nous avons une écriture plus compacte que la lambda en utilisant le module operator
et la fonction add
qui y est défini (entre autre chose) :
|
|
Donc reduce
permet de combiner l’ensemble des éléments d’une séquence en prenant en compte éventuellement une valeur initiale pour produire un résultat.
Cependant il est peu probable que vous utilisiez reduce
pour calculer une somme de nombres en Python. Pourquoi ? Parce qu’il y a une fonction native sum
qui est une version spécialisée de reduce
et qui calcule la somme des valeurs numériques d’un iterator
.
|
|
|
|
La fonction sum
prend également en paramètre optionel une valeur initiale qui vaut par défaut 0
.
Il faut noter que pour les nombres flottants il vaut mieux utiliser la fonction fsum
du module math
.
|
|
En forçant l’affichage de de décimal, on voit que le calcul n’a pas la même précision :
|
|
Entre nous, je trouve cela rigolo qu’une somme de nombres soit au fond une réduction !
On donne le max !
Après tout, reduce
peut être utilisé à autre chose que le calcul de la somme des nombres d’un itérable. On peut par exemple l’utiliser pour déterminer la maximum et le minimum d’une séquence d’élément.
|
|
|
|
Mais comme pour la fonction sum
, en Python, il existe des fonctions natives max
et min
dont on préférera l’utilisation à celles de reduce
.
|
|
|
|
C’est sûr en présentant les choses comme cela, on ne perçoit probablement pas grand intérêt à reduce
pour l’instant.
House of the Dragon
La fonction reduce
est souvent utilisée pour réduire une liste d’éléments à une valeur scalaire mais rien n’oblige à ce que cela soit le cas. On peut utiliser reduce
pour combiner les éléments pour produire n’importe quel type de valeur.
Ainsi imaginons que j’ai une liste de noms et que je veuille créer un dictionnaire qui a comme clé ce nom avec comme valeur associée la longueur de ce nom.
Je peux m’y prendre comme suit avec reduce
.
|
|
J’ai une fonction qui combine les éléments de mon iterator
en les aggrégeant les uns après les autres dans un dictionnaire avec sa taille. On part de l’hypothèse que la séquence n’a pas de doublon pour rester simple.
La valeur d’initialisation est le dictionnaire vide.
Le premier élément sera ajoutée dans ce dictionnaire vide et la valeur retournée sera le dictionnaire avec ce premier élément et sa taille.
Cette valeur retournée sera passée comme paramètre accumulator
pour le deuxième élément et ainsi de suite.
Arrivée en fin de liste, la valeur retournée par reduce
sera le dictionnaire avec le dernier élément ajouté.
|
|
Bien sûr en Python, vous pouvez obtenir le même résultat avec une compréhension, ce qui donne un code plus compact et probablement plus compact.
|
|
Il faut reconnaitre qu’encore une fois, il existe une solution en Python plus intéressante que celle de reduce
. Cependant, une compréhension pourra être utilisée à la place de reduce
si le résultat que l’on souhaite obtenir est une list
, un tuple
, un set
, un dict
ou un iterator
.
Si on souhaite réduire à un objet par exemple on ne pourra pas le faire directement avec une compréhension.
Prenons un exemple un peu artificiel basé sur l’exemple précédent.
|
|
La classe HousesOfWesteros
est juste une enveloppe autour d’un dictionnaire.
En soit elle n’a pas grand intérêt, si ce n’est pour les besoins de la démonstration ici.
En effet, ce qui est intéressant avec reduce
c’est qu’on peut réduire une séquence a à peu près n’importe quoi.
On notera au passage, l’utilisation du Duck Typing et la réutilisation de la fonction aggregate
définie précédemment, qui est utilisée aussi bien sur un dict
que sur la classe HousesOfWesteros
, ce qui est important est que l’on puisse appliquer sur l’un et l’autre la fonction update
.
Le point est que la fonction reduce
est très générique mais qu’en Python pour la plupart des besoins courants, on n’en n’aura pas besoin car on aura une solution plus simple à mettre en oeuvre.
La fonction reduce est vraiment générique
Comme cela vient d’être écrit la fonction reduce
est très générique, à tel point que l’on peut exprimer les fonctions filter et map avec reduce
.
D’une certaine manière filter et map sont des versions spécialisées de reduce
.
Commencçons par filter
.
|
|
On peut constater que cela nous donne bien le même résultat qu’avec le filter
natif.
|
|
L’implémentation repose sur la fonction de réduction qui réalise le filtrage de la liste originale. On notera quand même que dans l’implémentation que je propose c’est une liste qui est retournée et non un iterator
.
L’objet est ici bien sûr de montrer le principe de fonctionnement.
Si nous passons à map
, on pourrait avoir une implémentation de la forme :
|
|
On obtient des résultats similaires, au détail prêt encore une fois que l’implémentation proposée retourne une list
et non un iterator
.
|
|
Le principe est le même que pour filter
, la fonction de réduction crée la liste d’éléments transformés par l’application de la fonction passée à map
.
séquence, réduisant cette séquence grâce à cette fonction ; cette dernière doit prendre en paramètre 2 éléments du même type que les éléments de la séquence ; elle combine successivement chaque élément de la liste avec la fonction. Elle prend éventuellement un troisième paramètre optionnel qui sert de valeur de départ., appliquant la fonction sur les éléments de la séquence pour produire un résultat qui n’est pas nécessairement une séquence, dans le cas de reduce
. On verra plus loin que filter
et map
sont des spécialisations de reduce
.
Synthèse
La fonction reduce
est une fonction générique qui transforme une séquence d’éléments grâce à la fonction qui lui est passée en paramètre et qui sert à combiner les éléments de cette séquence 2 à 2 en la parcourant de la gauche vers la droite.
La fonction reduce
est extrêmement puissante et est une fonction clé aux traitements des séquences dans les langages fonctionnels.
Néanmoins en Python, si elle est présente, elle n’est souvent pas la solution privilégiée par rapport à des solutions plus spécialisées mais plus lisibles.
Les exemples de code du billet sont disponibles dans un gist.
Ressources
- Billet chapeau sur le traitement des séquences avec un style fonctionnel
- Billet sur filter
- Billet sur map
- Billet sur flatmap
- gist des exemples du billet
- Notion de Collection Pipeline
- Avec les opérations communes sur ce Collection Pipeline :
- Article Python’s reduce(): From Functional to Pythonic Style
- Article map(), filter() et reduce () ?
- RxJS Marbles
- RxPy
- ReactiveX
Auteur TGITS
Modifié 2021-05-07