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.

Pour pleinement profiter de ce billet, il faut que vous connaissiez déjà le fonctionnement de map et reduce en Python ou que vous lisiez d’abord les billets que j’ai écrit sur l’un et l’autre.

Le besoin

Dans les billets précédents sur filter, map et reduce, nous avons vu des fonctions natives ou pas qui existent en Python et permettent d’écrire du code avec un style fonctionnel.

Dans le billet sur map, nous avons comparé cette fonction à l’utilisation des compréhensions (qui lui sont souvent préférées en Python) en comparant par rapport aux exemples du billet qui leur était dédié.

Néanmoins, dans ce billet nous avions le cas d’une compréhension imbriquée pour représenter un échiquier sous la forme d’un tableau de pairs colonnes et lignes :

1
2
3
4
columns = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')
rows = (1, 2, 3, 4, 5, 6, 7, 8)
chessboard = [(c, r) for c in columns for r in rows]
print(chessboard)
1
[('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]

Comment obtenir la même chose avec map ?

Vers flatmap et au-delà

Dans un premier temps pourquoi ne pas partir sur une approche naïve dans laquelle on va avoir des fonctions map imbriquées, tout comme on a des boucles for imbriquée ?

1
print("Approche naïve (1) : ", list(map(lambda x: map(lambda y: (x, y), rows), columns)))

Cela nous donne une liste d’objets map.

1
Approche naïve (1) :  [<map object at 0x0000023F25AE9CA0>, <map object at 0x0000023F25AE9070>, <map object at 0x0000023F25AE6FD0>, <map object at 0x0000023F25AE6310>, <map object at 0x0000023F25AE6190>, <map object at 0x0000023F25D54C40>, <map object at 0x0000023F25D54CD0>, <map object at 0x0000023F25D54C10>]

Transformons ces objets map en des listes.

1
print("Approche naïve (2) : ", list(map(lambda x: list(map(lambda y: (x, y), rows)), columns)))

Nous obtenons, une liste de listes de pairs, ces dernières correspondant à ce que l’on souhaite comme données. Seulement on souhaiterait juste avoir une liste de pairs, pas une liste de liste de pairs.

1
2
Approche naïve (2) :  [[('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8)], [('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), 
('b', 8)], [('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8)], [('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8)], [('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8)], [('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8)], [('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8)], [('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]]

Avec la fonction map, on le voit l’implémentation ne va pas être directe et va demander d’utiliser d’autres fonctions. Dans le cadre d’une approche fonctionnelle, pour obtenir une solution élégante il faudrait s’appuyer sur une fonction de la forme flatmap en faisant un flatmap appelant le map. Le soucis est que la fonction flatmap n’existe pas en Python. Cependant Python ne nous laisse pas dépourvu de solutions pour arriver à nos fins.

Néanmoins avant tout chose peut-être un mot sur flatmap. Avec map vous transformez une séquence en une nouvelle séquence dans laquelle chaque élément est le résultat de l’appel de la fonction passée en paramètre.

Par exemple, avec une fonction très simple qui ajoute 1, exprimée sous forme de lambda sur la séquence des nombres de 0 à 9, on obtient la séquence des nombres de 1 à 10 :

1
2
sequence_1 = map(lambda x : x +1, range(0,10))
print(list(sequence_1))
1
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Je sais, je radote, j’ai déjà expliqué la fonction map : c’est juste un petit rappel pour arriver à flatmap.

Maintenant, modifions la fonction pour qu’elle retourne une paire avec la valeur passée en paramètre et cette valeur plus un :

1
2
sequence_2 = map(lambda x : [x, x + 1], range(0,10))
print(list(sequence_2))

Cette fois-ci, on n’obtient plus une séquence de nombres, mais une séquence de liste de nombres

1
[[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10]]

Ce n’est pas forcément gênant mais si ce que vous souhaitez exploiter au final c’est une liste de nombres et non une liste de listes de nombres, il y a un peu de travail à effectuer sur cette liste, pour l’aplatir, c’est-à-dire supprimer le niveau d’imbrication et obtenir la liste [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10]. C’est là où on pourrait utiliser flatmap si la fonction existait en Python. Cela donnerait ceci :

1
2
sequence_3 = flatmap(lambda x : [x, x + 1], range(0,10))
print(list(sequence_3))

qui aurait comme résultat :

1
[0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10]

Le fonction flatmap applique la fonction qui lui est passée en paramètre comme la fonction map mais concatène le résultat au reste de la séquence plutôt que de l’ajouter dans la séquence.

Si on essaie d’être un peu plus formel, la fonction map :

  • prend en paramètre
    • une fonction qui en paramètre un élément de type A et qui le transforme en élément de type B
    • une séquence d’éléments de type A
  • fournit en résultat une séquence d’éléments de type B

La fonction flatmap :

  • prend en paramètre
    • une fonction qui en paramètre un élément de type A et qui le transforme en élément de type Liste de B
    • une séquence d’éléments de type A
  • fournit en résultat une séquence d’éléments de type B

La fonction flatmap est importante dans les langages fonctionnelles et se retrouve au coeur de la notion de monade.

Maintenant que nous avons défini ce qu’est flatmap, comment peut-on l’implémenter en Python ?

Dans un premier temps revenons à notre problème concret avec l’échiquier.

Le résultat obtenu n’était pas si loin que cela de la cible. Il nous faudrait un moyen d’aplatir le résultat ou de concaténer l’ensemble des listes pour n’en faire qu’une. Tiens d’ailleurs dans certains langgaes (comme Clojure), la fonction s’appelle mapcat et non flatmap. Cela peut nous donner des pistes. En effet, dans le module operator de Python on peut remarquer iconcat qui correspond à += pour des séquences. Cela correspond bien à notre besoin : une fonction qui concatène deux séquences et retourne le résultat. Il faudrait maintenant une fonction qui nous permette d’appliquer cette concaténation successivement à chaque liste dans la liste. La fonction reduce.

1
2
3
4
5
6
7
8
import functools
import operator


columns = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')
rows = (1, 2, 3, 4, 5, 6, 7, 8)
chessboard = functools.reduce(operator.iconcat, map(lambda x: map(lambda y: (x,y), rows), columns), [])
print("Liste des cases de l'échiquier avec map, reduce et iconcat :", chessboard)

Cela nous donne bien le résultat attendu :

1
2
3
Liste des cases de l'échiquier avec map, reduce et iconcat : [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), 
('d', 6), ('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 
7), ('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]

Pour bien comprendre comment cela fonctionne, revenons au résultat avec l’approche naïve :

1
2
[[('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8)], [('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), 
('b', 8)], [('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8)], [('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8)], [('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8)], [('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8)], [('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8)], [('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]]

On a une liste de listes. Avec reduce, nous parcourons cette liste élément après élément, c’est-à-dire ici liste après liste, et nous les concaténons les unes après les autres avec la valeur obtenue à l’étape précédente, la valeur initiale étant la liste vide.

Cela nous guide vers une implémentation possible de flatmap.

1
2
3
4
5
6
import functools
import operator


def functional_flatmap(fun, iterator):
    return functools.reduce(operator.iconcat, map(fun, iterator))

Le paramètre fun est une fonction qui prend un paramètre correspondant au type produit par un iterator et retourne une list. On peut la tester sur la génération du tableau de l’échiquier.

1
2
3
chessboard = functional_flatmap(
    lambda x: list(map(lambda y: (x, y), rows)), columns)
print("\nListe des cases de l'échiquier avec functional_flatmap :", chessboard)
1
2
Liste des cases de l'échiquier avec functional_flatmap : [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), 
('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]       

Ce qui nous donne bien le résultat attendu. Cependant on notera que notre fonction fun doit retourner une liste ou du moins un type compatible avec operator.iconcat.

On peut bien sûr faire une version de flatmap impérative qui retournera elle-aussi une liste.

1
2
3
4
5
def imperative_flatmap(fun, iterator):
    resulting_iterator = []
    for elem in iterator:
        resulting_iterator.extend(fun(elem))
    return resulting_iterator

Si on l’applique à la génération de la liste des cases de l’échiquier :

1
2
3
chessboard = imperative_flatmap(
    lambda x: list(map(lambda y: (x, y), rows)), columns)
print("\nListe des cases de l'échiquier avec imperative_flatmap :", chessboard)

On obtient un résultat similaire.

1
2
Liste des cases de l'échiquier avec imperative_flatmap : [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), 
('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]       

Néanmoins si ces solutions sont intéressantes car elles nous permettent de comprendre le fonctionnement de flatmap et de voir pour la version fonctionnelle une exemple de combinaison de reduce et map, elles ne sont pas pleinement satisfaisantes.

Nous allons voir comment itertools va pouvoir nous aider à trouver des solutions plus satisfaisantes.

Itertools for the win

Et splat le chien

Il est possible de développer une liste ou un de manière plus générale un iterator avec *, le splat operator : * développe un iterator en l’ensemble de ces éléments. Illustrons cela.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import itertools


example_liste = ["a", "b", "c", "d"]
example_range = range(0,10)
example_iterator = itertools.islice(itertools.count(10), 0, 10)
print("Une liste exemple :", example_liste)
print("Une liste exemple développée avec * :", *example_liste)
print("Un intervalle exemple :", example_range)
print("Un intervalle exemple développée avec * :", *example_range)
print("Un iterateur exemple :", example_iterator)
print("Un iterateur exemple développée avec * :", *example_iterator)

Ce code donne comme affichage :

1
2
3
4
5
6
Une liste exemple : ['a', 'b', 'c', 'd']
Une liste exemple développée avec * : a b c d
Un intervalle exemple : range(0, 10)
Un intervalle exemple développée avec * : 0 1 2 3 4 5 6 7 8 9
Un iterateur exemple : <itertools.islice object at 0x000001C9E3EADE00>
Un iterateur exemple développée avec * : 10 11 12 13 14 15 16 17 18 19

Cet opérateur splat est bien pratique et va pouvoir être utilisé en conjonction avec une fonction de itertools

Iterator unchained

Dans le module itertools il y a la fonction chain qui prend un ou plusieurs iterables et retourne un iterator. Cet iterator est en quelque sorte la concaténation des iterables fournis en entrée de chain : les éléments de chaque iterable sont retournés en les uns après les autres avant de passer à l'iterable suivant. Un exemple permettra d’éclairer mon propos.

1
2
3
4
list_0_to_9 = range(0,10)
list_10_to_19 = itertools.islice(itertools.count(10), 0, 10)
list_20_to_25 = itertools.islice(itertools.count(20), 0, 6)
print("Exemple d'utilisation de chain, nombres de 0 à 25 :", *itertools.chain(list_0_to_9, list_10_to_19, list_20_to_25))

Comme on peut le voir ci-après, ces lignes vont provoquer l’affichage des nombres de 0 à 25 : chain a produit un iterator qui va d’abord retourner les valeurs 0 à 9 provenant de list_0_to_9, puis les valeurs provenant de list_10_to_19 et enfin celles provenant de list_20_to_25.

1
Exemple d'utilisation de chain, nombres de 0 à 25 : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

On notera au passage l’utilisation de itertools.islice qui permet de sélectionner un ensemble d’éléments d’un iterable et itertools.count qui produit un iterator qui retourne des valeurs espacées régulièrement à partir d’une valeur de départ (ici l’espacement est la valeur par défaut 1, pour une valeur de départ valant respectivement 10 et 20).

Armé de * et de chain nous allons pouvoir construire une nouvelle version de flatmap qui retournera cette fois-ci un iterator.

Tout d’abord, avant de généraliser à une fonction, voyons comment on peut les utiliser pour constituer la liste des cases de l’échiquier.

1
2
3
chessboard = itertools.chain(
    *map(lambda x: map(lambda y: (x, y), rows), columns))
print("\nListe des cases de l'échiquier avec map et chain :", list(chessboard))

Cela nous donne bien ce que nous attendons.

1
2
3
Liste des cases de l'échiquier avec map et chain : [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), 
('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 
8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]

En effet, le résultat des 2 maps imbriqués est un iterator produisant lui même des iterators correspondant respectivement à la colonne des 'a', puis à la colonne des 'b' est ainsi de suite. En appliquant le splat operator * sur ce résultat, l'iterator d'iterators est développé en une série iterators, qui sont passés à chain qui produit les éléments de chaque iterator les uns à la suite des autres, passant à l'iterator suivant dès que l'iterator courant est épuisé.

De là il est facile de généraliser à une fonction proposant une nouvelle implémentation de flatmap.

1
2
3
4
5
import itertools


def itertools_flatmap(fun, iterator):
    return itertools.chain(*map(fun, iterator))

Si nous l’appliquons à la génération de la liste des cases de l’échiquier

1
2
chessboard = itertools_flatmap(lambda x: map(lambda y: (x, y), rows), columns)
print("\nListe des cases de l'échiquier avec itertools_flatmap :", list(chessboard))

Cela nous donne bien ici encore ce que nous attendons.

1
Liste des cases de l'échiquier avec itertools_flatmap : [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), ('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]        

Si fun est une fonction retournant un iterable, le résultat de map(fun, iterator) sera un iterator produisant des iterables. L’opérateur * permet de développer l'iterator de iterables en une série de iterables ; de là chain produira les valeurs de chacun des iterables l’un à la suite de l’autre.

Product

Néanmoins, pour réaliser un produit cartésien comme on le fait dans le cas de la liste des cases de l’échiquier, il y a une solution plus simple en exploitant toujours itertools : la fonction product qui réalise le produit cartésiens des iterables qu’on lui fournit en entrée.

1
2
3
4
5
6
7
import itertools


columns = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h')
rows = (1, 2, 3, 4, 5, 6, 7, 8)
chessboard = itertools.product(columns, rows)
print("Liste des cases de l'échiquier avec product:", list(chessboard))

Cela donne bien le résultat escompté

1
2
3
Liste des cases de l'échiquier avec product: [('a', 1), ('a', 2), ('a', 3), ('a', 4), ('a', 5), ('a', 6), ('a', 7), ('a', 8), ('b', 1), ('b', 2), ('b', 3), ('b', 4), ('b', 5), 
('b', 6), ('b', 7), ('b', 8), ('c', 1), ('c', 2), ('c', 3), ('c', 4), ('c', 5), ('c', 6), ('c', 7), ('c', 8), ('d', 1), ('d', 2), ('d', 3), ('d', 4), ('d', 5), ('d', 6), ('d', 
7), ('d', 8), ('e', 1), ('e', 2), ('e', 3), ('e', 4), ('e', 5), ('e', 6), ('e', 7), ('e', 8), ('f', 1), ('f', 2), ('f', 3), ('f', 4), ('f', 5), ('f', 6), ('f', 7), ('f', 8), ('g', 1), ('g', 2), ('g', 3), ('g', 4), ('g', 5), ('g', 6), ('g', 7), ('g', 8), ('h', 1), ('h', 2), ('h', 3), ('h', 4), ('h', 5), ('h', 6), ('h', 7), ('h', 8)]

La fonction product est une alternative intéressante à des boucles for imbriquées. Elle prend un ou plusieurs iterables en entrée, ainsi qu’un paramètre optionnel spécifiant le nombre de répétitions dont la valeur par défaut vaut 1. Elle mériterait comme la plupart des fonctions de itertools qu’on s’attarde sur elle mais cela sortirait du cadre de ce billet. Un sujet pour une autre fois.

Ne pas confondre flatmap et flatten

Un dernier point, il y a une autre opération classique sur les séquences qui peut faire un peu penser à flatmap, c’est flatten. Elle permet d’aplatir une séquence ayant un niveau arbitrairement profond d’imbrications.

La fonction flatmap ne réalise effectivement l’aplatissement qu’au premier niveau d’imbrication et cet aplatissement est implicitement fait à la suite de la transformation d’un élément de la séquence par une fonction de transformation qui transforme cet élément en une séquence.

La fonction flatten n’effectue pas de transformation des éléments, elle se contente de supprimer toutes imbrications dans la séquence. Ainsi la liste [1,[2,3,4],[5[6,[7]]],8,[[[9]]]] serait transformée en [1,2,3,4,5,6,7,8,9] par une fonction flatten. Il n’existe pas de fonction flatten dans la bibliothèque standard de Python.

Comme pour product, flatten mériterait qu’on s’y attarde mais cela sortirait du cadre de ce billet. Là encore, un sujet pour une autre fois.

Synthèse

Ce billet clos une série de billets sur le traitement des séquences avec un style fonctionnel en Python dans laquelle on a abordé filter, map et reduce.

Cela a été l’occasion de s’attarder sur la fonction flatmap et de regarder comment l’implémenter de différentes manières en Python. Cette exploration en soit contient son propre intérêt, mais il faut reconnaitre qu’en Python le besoin de recourir à filter, map et reduce est marginal, Python proposant des alternatives plus idiomatiques.

La fonction flatmap n’existe pas en Python en tant que fonction native ou en tant que fonction de la bibliothèque standard. Il est assez facile d’un coder une implémentation en s’appuyant sur itertools si c’est nécessaire, sachant que selon le besoin, itertools a peut-être déjà une solution prête à l’emploi.

Les exemples de code du billet sont disponibles dans un gist.

Ressources