Cette page contient un ensemble d’exemples montrant comment utiliser
des fonctions d’ordre supérieur (des fonctions prenant en paramètre
d’autrs fonction, comme filter, map, reduce …) pour
manipuler des ensembles de données. La seconde partie montre comment,
dans des conditions où les transformations sont pures (i.e ne font pas
d’effet de bord), le calcul peut être parallèlisé.
1ère partie : les fonctions d’ordre supérieur
Un exemple très simple de fonction d’ordre supérieur est la fonction
appelée map, qui prend en paramètre une liste $l$ et une fonction
$f$, applique la fonction à tous les éléments de $l$ et renvoie la
liste des résultats :
Cette fonction existe en Python et possède sa propre documentation, et en voici un cas d’usage :
>>> numbers = [1, 2, 3, 4, 5]
>>> sqrList = map(lambda x: x*x, numbers)
>>> sqrList
<map object at 0x7fd20e91ad50>
>>> list(sqrList) # must convert back to list to see contents
[1, 4, 9, 16, 25]
Le Python complique un peu les choses, parce qu’il permet de faire les opérations précédentes en utilisant la syntaxe des compréhensions de liste :
>>> numbers = [1, 2, 3, 4, 5]
>>> sqrList = [ x*x for x in numbers]
>>> sqrList
[1, 4, 9, 16, 25]
Écrire la fonction map comme une fonction Python. Écrire la fonction
filter qui prend en paramètre une fonction de filtre $f$ et une
liste $l$, et renvoie la liste des éléments de $l$ pour lesquels $f$
renvoie True. Écrire la fonction sum qui prend en paramètre une
fonction $f$ et une liste $l$, et qui renvoie la somme des $f(l_i)$.
Écrire une fonction reduce qui suit la spécification suivante (une
écriture récursive est la manière la plus simple de faire) :
En pratique, cette fonction calcule la chose suivante :
\[\mathrm{reduce}(f, acc, [l_1,l_2,\dots,l_n]) = f (\dots (f (f(acc, l_1), l_2) \dots), l_n)\]Noter que cette fonction existe dans le module functools et possède
une
documentation.
Réécrire la fonction sum à partir de la fonction reduce. Écrire à
partir de la fonction reduce une fonction any qui teste si une
liste de booléens contient un élément valant True. Difficile Utiliser la fonction reduce pour
écrire une fonction reverse qui prend une liste et renvoie la liste
symétrique.
2ème partie : sagesse de l’Antiquité
Les fonctions évoquées précédemment servent en quelque sorte de “couteau-suisse” pour effectuer des transformations sur des ensembles de données. Un usage courant de ces fonctions est la manipulation de données héritées d’une base de données. Pour simuler la chose ici, on propose de construire une petite base de données comme la liste suivante :
[
{ "name": "Thalès", "birth": -625, "death": -547 },
{ "name": "Anaximandre", "birth": -600, "death": -546 },
{ "name": "Héraclite", "birth": -544, "death": -480 },
{ "name": "Empédocle", "birth": -490, "death": -430 },
]
Utiliser à bon escient les fonctions map, filter, sort et
reduce pour effectuer les calculs suivants :
-
extraire de cette base la liste des noms dans l’ordre alphabétique;
-
extraire de cette base la personne née le plus tôt dans l’ordre chronologique;
-
extraire de cette base la personne dont la durée de vie a été la plus longue;
-
extraire de cette base la moyenne des durées de vies de ces sages.
3ème partie Pour aller plus loin … : questions de parallélisation
Hormis la possibilité de pouvoir manipuler des données, les fonctions
comme map permettent de mettre en place du parallélisme de manière
(plus ou moins) automatique. Le module processing permet ainsi de
distribuer des calculs sur des ensembles (pools) de processeurs. Par
exemple :
import multiprocessing
import requests
import time
def scrape_fun(url):
print("Scraping '{}'".format(url))
res = requests.get(url)
print("Returned {} ({})".format(res.url, res.status_code))
return res
def parallel_scrape():
num_workers = 5
all_urls = [
# insert here list of URLs
"http://perdu.com/",
"http://worldtimeapi.org/api/timezone/Europe/Paris",
]
with multiprocessing.Pool(num_workers) as pool:
results = pool.map(scrape_fun, all_urls)
print(results)
Le choix d’une fonction comme requests.get permet d’assurer que les
temps de retours des différents serveurs soient non triviaux et
dépendant de suffisamment de conditions extérieures pour paraître
aléatoires.
Tester le code précedent avec une liste d’URLs choisie avec
délicatesse pour ne pas surcharger les serveurs. Considérer le fait
que la parallélisation ne fonctionne bien uniquement parce que les
fonctions comme scrape ne font pas (trop) d’effets de bords (i.e
leurs appels sont indépendants).
Remarque : dans le cas où l’on ne désirerait pas faire de requêtes sur
des sites web externes, ou simplement parce que ce n’est pas possible,
on peut remplacer la requête par un calcul “long”. La façon la plus
simple de le faire est d’utiliser time.sleep :
def work_fun(value):
print(f"Working for {value} seconds")
time.sleep(value)
def parallel_do():
num_workers = 5
all_values = [ 1, 2, 3 ]
with multiprocessing.Pool(num_workers) as pool:
results = pool.map(work_fun, all_values)
print(results)
On constate (normalement, sur une machine avec plusieurs processeurs)
que le code précédent ne prend que 3 secondes, au lieu des 6
nécessaires pour faire l’ensemble des “calculs”. Noter que
l’utilisation de la bibliothèque multiprocessing impose que la
fonction work_fun soit définie au toplevel pour des raisons techniques.