Notes d'apprentissage de Python : traitement des séquences avec un style fonctionnel - map
Contenu
Contexte
Ce billet s’inscrit dans une série de billets sur le traitement des séquences avec un style fonctionnel en Python.
NB1 : La version de Python utilisée dans les exemples de code est la version 3.
NB2 : Ce billet a été modifié le 24/05/2021, par l’ajout d’un paragraphe sur le comportement de map
avec plusieurs itérables.
Principe
La fonction map
est native tout comme filter et prend également en paramètre une fonction et une séquence d’éléments.
La fonction map
permet d’appliquer cette fonction à chaque élément de la séquence et produit une nouvelle séquence résultante dont chaque élément est le résultat de l’application de cette fonction sur chaque élément de la séquence initiale.
Ainsi map
produit une nouvelle séquence dont chaque élément est l’élément correspondant de la première liste sur laquelle on a appliquée la fonction passée en paramètre.
Tout comme filter
, la séquence retournée par la fonction map
est un objet iterator
.
Voyons tout cela à travers un exemple simple dans lequel on transforme une séquence de nombres entiers en une séquence des carrés correspondants.
|
|
Si vous exécutez ce code avec Python vous obtiendriez quelque chose de similaire à ce qui suit :
|
|
Comparaison avec les boucles for et les compréhensions
L’équivalent avec une boucle for
pourrait s’écrire comme suit :
|
|
Qui produirait l’affichage suivant :
|
|
Ce n’est pas strictement équivalent car comme on peut le constater dans l’exemple, map
retourne un iterator
. Pour faire quelque chose de vraiment équivalent, il faudrait utiliser une fonction génératrice comme ci-après.
|
|
Qui après exécution donne quelque chose de similaire à ce qui suit :
|
|
Bien sûr on peut écrire du code équivalent sous forme de compréhension, ce qui est considéré comme un style plus pythonique car plus lisible :
|
|
La fonction map retourne un iterator
La fonction map
retourne un iterator
(comme la fonction filter) comme cela a été évoqué dans le paragraphe précédent.
Ainsi, le retour de la fonction map
peut être manipulé comme tel. On peut par exemple utiliser le résultat directement avec les fonctions (natives) sum
, max
ou min
.
|
|
Ce qui donne :
|
|
Et si nous voulons une liste ou un ensemble à partir du résultat de note map
, il faut le convertir vers le type approprié.
Par exemple pour transformer le résultat de map
vers une liste :
|
|
|
|
Ou encore pour transformer le résultat de map
en un ensemble :
|
|
|
|
On peut également convertir l'iterator
obtenu avec map
vers un dictionnaire si ses éléments correspondent à des paires, de la même manière que l’on peut convertir une liste de paires vers un dictionnaire.
|
|
Dans l’exemple ci-dessus, la fonction passé à map
retourne un tuple
et non plus juste une valeur scalaire. Cela nous donne le résultat ci-après.
|
|
Il est possible tout comme avec filter, les compréhensions) et de manière générale avec les iterator
en Python, de manipuler des séquences potentiellement infinies (en exploitant par exemple le module itertools
). Voici un exemple :
|
|
En utilisant ici la fonction count
du module itertools
, je génère un iterator
infini de nombres entiers à partir de 1 et j’applique la fonction map
dessus. Avec la fonction takewhile
, je prends des valeurs de l'iterator
généré par map
tant que les valeurs sont strictement inférieures à 1000.
|
|
Combiner map et filter
Avec les compréhensions on peut effectuer un filtrage sur les éléments.
Avec map
on ne peut pas le faire aussi directement que dans une compréhension mais on peut bien sûr combiner map
et filter
pour avoir le même résultat.
|
|
Ici on a fait le choix de travailler en 2 temps, en créant une variable intermédiaire pour l'iterator
produit par filter
.
Bien sûr on pourrait passer directement filter
comme paramètre de map
.
Néanmoins les expressions de cette forme deviennent vite peu lisibles.
|
|
Contrairement à des langages fonctionnelles comme Elixir ou Clojure, il n’y a pas de sucre syntaxique comme le pipe operator |>
ou des threading macros comme ->>
pour faciliter l’écriture d’une chaîne d’opération sur des iterator
.
De plus, on ne peut pas enchainer les fonctions comme map
, filter
, reduce
ou les fonctions de itertools
comme on le ferait avec les stream en Java par exemple, car justement ce sont des fonctions, pas des méthodes de l’objet iterator
.
Il est donc souvent préférable pour des questions de lisibilité de passer par des variables intermédiaires pour éviter d’avoir des appels de fonctions imbriqués.
Il y a plusieurs manières d’obtenir l’équivalent d’une boucle ou d’une compréhension imbriquée avec map
, mais cela implique d’autres fonctions que juste map
. Ce point est développé dans le billet sur flatmap
La fonction map en Python peut traiter plusieurs itérables
La fonction map
en Python peut en fait prendre plusieurs itérables, pas juste un seul.
Il faut que la fonction qu’applique map
prenne elle-même en paramètre autant d’arguments qu’il y a d’itérables fournis.
Chaque élement séquence produite par map
est résultat de l’application de la fonction sur les éléments correspondants des différents itérables.
Un exemple pour clarifier :
|
|
La fonction lambda prend 3 arguments et réalise leur concaténation en prenant respectivement le premier élément de chacun des itérables, puis le second, etc.
|
|
Avec une compréhension, vous obtiendriez le même résultat avec un code similaire à ce qui suit :
|
|
On notera l’utilisation de la fonction zip
dans la compréhension, c’est ce que fait map
avec plusieurs itérables d’une certaine manière, ils sont zippés implicitement.
Dans itertools
, il existe une variante de le fonction map
qui comme dans l’exemple de la compréhension fonctionnerait à partir d’un ensemble d’itérables zippés ou à partir d’un itérable de tuples, la fonction starmap
.
|
|
ou à partir d’une liste de tuples directement
|
|
Ce qui dans un cas comme dans l’autre, donnerait le même résultat qu’avec map
.
Pour résumer le fonctionnement de map
avec cet exemple.
A comparer avec le fonctionnement de starmap
.
Si vous avez plusieurs itérables à partir desquels vous souhaiteriez produire une nouvelle séquence en fonction de leurs éléments de même indice, map
peut vous éviter d’utiliser la fonction zip
et c’est peut-être un exemple où vous la préférerez à l’utilisation d’une compréhension.
Si vous avez directement un itérable avec un tuple d’éléments à partir duquel vous voulez produire une nouvelle séquence dont les éléments sont construits à partir des composants du tuple, starmap
peut être à envisager.
Attardons nous sur quelques autres exemples inspirés de la documentation de la bibliothèque standard de Python.
Si nous voulons avoir une liste des nombres entre 1 et 9 élevés à la puissance d’eux-mêmes, on pourrait écrire quelque chose de la forme :
|
|
Ce qui nous donnerait l’affichage suivant :
|
|
On peut également travailler avec des séquences (potentiellement) infinies.
La fonction repeat
de itertools
produit une séquence infinie de la valeur qu’on lui a passé en paramètre.
Pour produire la liste des puissance de 2 des nombres de 1 à 9, on pourrait écrire quelque chose de la forme :
|
|
Comme pour la fonction zip
, c’est la plus courte des 2 séquences qui déterminera la taille de la séquence produite.
La fonction repeat(2)
produit ici une liste potentiellement infini de 2
mais seulement 9 valeurs se retrouveront utilisées pour la liste finale produite par map
.
|
|
Il est également possible que toutes les séquences passées à map
soit infinies.
En partant de l’exemple précédcent, une manière d’avoir la liste des puissances des nombres entiers à partir de 1 et d’afficher les 20 premiers pourrait être la suivante :
|
|
La fonction count
de itertools
génère une liste de valeurs à partir d’une valeur de départ (entière ou réelle) et d’un pas de progression (lui aussi entier ou réel), la valeur par défaut de ce pas étant la valeur 1
(qui sera la valeur du pas dans notre exemple).
Ici count(1)
produit la séquence 1, 2, 3, ...
.
On utilise islice
toujours de itertools
pour extraire l’intervalle des 20 premiers éléments de l'iterator
produit par map
, en précisant l’itérable duquel sélectionné la valeur de départ, l’index de départ et l’index de fin (non-inclus).
|
|
Synthèse
La fonction map
est une fonction native de Python. Elle prend en paramètre une fonction et un iterator
; elle retourne un nouvel iterator
dont chaque élément est le résultat de l’application de cette fonction sur chaque élément de la séquence initiale.
La fonction map
peut être vue comme l’abstraction d’une boucle sur une liste pour appliquer une fonction sur chaque élément de la liste et produire une nouvelle liste résultante.
La fonction map
en Python peut prendre plusieurs itérables en paramètres.
Il est bien sûr possible de combiner map
avec filter
pour filtrer avant d’appliquer map
ou au contraire après sur l'iterator
produit par map
.
L’ensemble du code du billet est disponible dans un gist.
Ressources
- Billet chapeau sur le traitement des séquences avec un style fonctionnel
- Billet sur filter
- Billet sur reduce
- Billet sur flatmap
- gist des exemples du billet
- Notion de Collection Pipeline
- Avec les opérations communes sur ce Collection Pipeline :
- Article map(), filter() et reduce () ?
- RxJS Marbles
- RxPy
- ReactiveX
Auteur TGITS
Modifié 2021-05-29