Programmation fonctionnelle en Python

publicité
Programmation fonctionnelle en Python
adaptation libre de la documentation officielle
Table des matières
Programmation fonctionnelle en Python.........................................................................1
Introduction........................................................................................................................ 1
Preuve formelle.......................................................................................................3
Modularité................................................................................................................3
Facilité de débogage et de test...............................................................................3
Composabilité..........................................................................................................4
Itérateurs............................................................................................................................ 4
Les types de données qui supportent les itérateurs...............................................6
Expressions génératrices et compréhension des listes.................................................7
Générateurs........................................................................................................................ 9
Passer des valeurs à un générateur.....................................................................11
Fonctions prédéfinies......................................................................................................12
Le module itertools.......................................................................................................... 14
Créer de nouveaux itérateurs...............................................................................14
Appels de fonctions sur des éléments..................................................................16
Sélectionner des éléments....................................................................................16
Grouper des éléments...........................................................................................17
Le module functools........................................................................................................17
Le module operator...............................................................................................19
Petites fonctions et expression lambda.........................................................................19
Dans ce document, nous ferons une excursion dans les données de Python qui conviennent à
l'implémentation de programmes dans le style fonctionnel. Après une introduction aux
concepts de la programmation fonctionnelle, nous jetterons un œil sur les données telles les
itérateurs (iterators) et les générateurs (generators) et les modules qui en relèvent tels que
itertools et functools.
Introduction
Cette section explique les concepts de base de la programmation fonctionnelle : si vous vous
intéressez uniquement aux caractéristiques du langage Python, passez à la section suivante.
Les langages de programmation réalisent la décomposition des problèmes de plusieurs
façons :
* la plupart des langages sont procéduraux : les programmes sont des listes
d'instructions qui disent à la machine ce qu'il faut faire des données d'entrées fournies
du programmes. C, Pascal, et même le shell d'Unix sont des langages procéduraux.
* dans les langages déclaratifs, vous écrivez une spécification qui décrit le problème à
résoudre, et le langage implémente les figures sur la façon de réaliser efficacement le
calcul. SQL est le langage déclaratif qui vous est sûrement le plus familier ; une
requête SQL décrit les données que vous désirez retrouver et la machine SQL décide
adaptation de la doc Python
page 1
Programmation fonctionnelle en Python
quelles tables ou quels index elle examine, quelles clauses partielles doivent être
calculées en priorité etc.
* la programmation orientée objets manipule des collections d'objets. Les objets ont
un état interne et supportent des méthodes qui requièrent ou modifient l'état interne
comme il le faut. Smalltalk et Java sont des langages orientés objets. C++ et le langage
Python supportent la programmation orientée objets mais ne forcent pas à utiliser les
caractéristiques de l'orienté objets.
* la programmation fonctionnelle décompose un problème en un ensemble de
fonctions. Dans l'idéal, les fonctions prennent des entrées, et produisent des sorties, et
n'ont aucun état interne qui affecte les sorties produites pour des données d'entrées
données. Parmi les plus connus des langages fonctionnelle, citons la famille ML
(Standard ML, Ocaml, et d'autres variantes) et Haskell.
NDT : Haskell est un langage destiné à la recherche sur la programmation. C'est un langage
fonctionnel pur, ce qui signifie que par défaut aucun effet de bord n'est autorisé, ni les
entrées/sorties, ni même l'affectation d'une variable. Haskell impose ce style dans tout code
qui ne signale pas explicitement par son type qu'il contient des effets de bord.
Les concepteurs de certains langages informatiques choisissent de mettre l'accent sur une
approche particulière de programmer. Cela peut rendre difficile d'écrire des programmes qui
utilisent une approche différente. D'autres langages sont des langages multi-paradigmes qui
supportent plusieurs approches différentes. Lisp, C++, Python sont multi-paradigmes. Vous
pouvez écrire des programmes ou des librairies qui sont largement procéduraux, ou orientés
objets, ou fonctionnels avec tous ces langages. Dans un grand programme, des sections
différentes peuvent être écrites selon des approches différentes ; par exemple, les unités
d'interface graphique peuvent être orientés objet alors que la logique du calcul est procédurale
ou fonctionnelle.
Dans un programme fonctionnel, l'entrée remonte à travers un ensemble de fonctions. Chaque
fonction opère sur son entrée et produit une nouvelle entrée. Le style fonctionnel décourage
les fonctions qui on un effet de bord qui modifie l'état interne ou réalise d'autres changements
qui ne sont pas visibles dans la valeur retournée par la fonction. Les fonctions sans effet de
bord sont dites purement fonctionnelles. Éviter les effets de bord, cela signifie qu'on ne peut
utiliser de structures de données qui sont mises à jour quand le programme tourne ; chaque
sortie d'une fonction doit uniquement dépendre de ses entrées.
Certains langages sont très stricts sur la pureté et ne possèdent pas l'instruction d'affectation
comme a=3 ou c=a+b, mais il difficile d'éviter tous les effets de bord. Afficher à l'écran ou
écrire un fichier sur un disque sont des effets de bord. Par exemple en Python, l'appel des
fonctions print() ou time.sleep() ne retourne rien de bien utile ; ces fonctions sont
uniquement appelées pour leur effet de bord qui est d'envoyer du texte à l'écran ou d'arrêter
l'exécution durant une seconde.
Les programmes Python écrits dans un style fonctionnel ne tombent pas dans ces extrêmes
que sont l'évitement de toute entrée/sortie ou de toute affectation ; ainsi, ils auront une
interface d'apparence fonctionnelle, mais utiliseront des caractères non fonctionnels en
interne. Par exemple, l'implémentation d'une fonction va utiliser l'affectation à des variables
locales, mais évitera de modifier des variables globales et autres effets de bord.
On peut considérer la programmation fonctionnelle comme l'exact opposé de la
programmation orientée objets. Les objets sont un peu comme des capsules qui contiennent
des états internes avec une collection de méthodes qui quand vous les appelez, modifient ces
états, et un programme consiste à réaliser le bon ensemble de ces changement d'états. La
programmation fonctionnelle désire éviter les changements d'état autant que possible et
adaptation de la doc Python
page 2
Programmation fonctionnelle en Python
procède par transmission de données entre des fonctions. Python permet de combiner les deux
approches en écrivant des fonctions qui acceptent et retournent des instances qui représentent
les objets de votre application (e-mail, transactions, etc).
La conception fonctionnelle peut apparaître comme une étrange contrainte dans le travail.
Pourquoi éviter les objets et les effets de bord ? Il y a des avantages théoriques et pratiques à
adopter le style fonctionnel :
* la preuve formelle
* la modularité
* la composabilité
* la facilité de débogage et de test.
Preuve formelle
Un bénéfice théorique : il est facile de construire une preuve mathématique qu'un
programme fonctionnel est correct.
Pendant longtemps, les chercheurs se sont consacré à trouver les moyens de prouver la
correction d'un programme. Ceci est différent de tester un programme avec des entrées
suffisamment nombreuses pour conclure que les sorties sont normalement correctes, ou de lire
le code source d'un programme et d'en déduire que le code paraît bon ; le but est au contraire
de fournir une preuve rigoureuse que le programme produit le bon résultat pour toutes les
entrées possibles.
La technique utilisée pour prouver la correction d'un programme est d'exhiber les invariants,
propriété des données entrées et des variables du programme qui sont toujours vraies. Pour
chaque ligne de code, on peut montrer que si les invariants X et Y sont vrais avant que la ligne
soit exécutée, les invariants légèrement différents X' et Y' sont vrais après que la ligne soit
exécutée. On continue le procédé jusqu'à atteindre la fin du programme, et à ce niveau, les
invariants doivent vérifier les conditions désirées sur la sortie du programme.
L'évitement des affectations en programmation fonctionnelle provient du fait que les
affectations sont difficiles à manier avec la technique de preuve ; les affectations peuvent
casser les invariants qui étaient vrais avant l'affection en ne produisant pas un nouvel invariant
qui peut se propager vers la suite.
Malheureusement, prouver qu'un programme est correct est largement impraticable et ne
relève pas de la programmation en Python. Même les programmes triviaux requièrent ds
preuves qui remplissent plusieurs pages ; la preuve de correction pour un programme un peu
compliqué serait énorme, et peu de programmes, voire aucun ne le fait aujourd'hui ;
l'interpréteur Python, l'analyseur XML ou votre navigateur devraient être prouvés. Même si
vous écrivez ou engendrez une preuve, vous vous poserez le question de la vérification de la
preuve ; peut-être contient-elle une erreur, et vous croirez vraiment que votre programme est
correct.
Modularité
Un bénéfice plus pratique de la programmation fonctionnelle est qu'elle contraint à découper
votre problème en petits morceaux. Il en résulte des programme plus modulaires. Il est facile
de spécifier et d'écrire une petite fonction qui fait une seule chose plutôt qu'une grande
fonction qui fait des transformations complexes. Les petites fonctions sont aussi plus facile à
lires et à vérifier.
Facilité de débogage et de test
Tester et déboguer un programme en style fonctionnel es plus facile.
adaptation de la doc Python
page 3
Programmation fonctionnelle en Python
Le débogage est simplifié parce que les fonctions sont en général petites et clairement
spécifiées. Quand un programme ne fonctionne pas, chacune des fonctions est un point
d'interface où l'on peut vérifier que les données sont correctes. Vous pouvez voir les entrées et
sorties intermédiaires et rapidement isoles la fonction responsable du bug.
Il est facile de tester parce que chaque fonction est un sujet potentiel pour un test unitaire. Les
fonctions ne dépendent pas d'un état du système qui a besoin d'être reprroduit avant de lancer
un test ; à la place vous avez seulement à synthétiser les bonnes entrées et à vérifier que les
sorties correspondent aux attentes.
Composabilité
Quand vous travaillez sur un programme de style fonctionnel, vous allez écrire plusieurs
fonctions avec des entrées et sorties diverses. Certaines de ces fonctions seront inévitablement
spécialisées à une application particulière, mais d'autres pourront servir dans une grand
variété de programmes. Par exemple, une fonction qui prend un chemin de répertoire et
retourne les fichiers XML du répertoire, ou une fonction qui prend un nom de fichier et
retourne son contenu peuvent être appliquées dans de nombreuses situation différentes.
Avec le temps, vous constituerez une librairie personnelle d'utilitaires. Souvent vous
assemblerez de nouveaux programmes en agençant des fonctions existantes dans des
configurations nouvelles et vous écrirez des fonctions spécialisées pour le travail en cours.
Itérateurs
Je vais commencer en examinant une caractéristique Python qui est fondamentale pour écrire
des programmes de style fonctionnel : les itérateurs (iterators).
Un itérateur est un objet qui représente un flux de données ; cet objet fournit ses données un
élément à la fois. Un itérateur Python doit supporter la méthode appelée __next__() qui ne
prend aucun argument et retourne toujours l'élément qui suit du flux de données. S'il n'y a pas
d'élément suivant dans le flux, __next__() doit lever une exception StopIteration. Les
itérateurs ne sont pas nécessairement finis cependant ; il est parfaitement raisonnable d'écrire
un itérateur qui produit un flux infini de données.
La fonction prédéfinie iter() prend un objet quelconque et essaie d'en retourner un itérateur,
qui va retourner les contenus de l'objet ou éléments, produisant un TypeError si l'objet de
peut pas supporter l'itération. De nombreux types de données prédéfinis supportent l'itération,
les exemples les plus communs étant les listes et les dictionnaires. Un objet est appelé
itérable si vous pouvez construire un itérateur à partir de lui.
On peut expérimenter l'interface d'itération de façon manuelle :
>>> L = [1, 2, 3]
>>> it = iter (L)
>>> it
<list_iterator object at 0x7fbd29398dd0>
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
3
>>> it.__next__()
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
it.__next__()
StopIteration
adaptation de la doc Python
page 4
Programmation fonctionnelle en Python
>>> it = iter (L)
>>> next (it)
1
>>> next (it)
2
>>> next (it)
3
>>> next (it)
Traceback (most recent call last):
File "<Shelley#11>", line 1, in <module>
next (it)
StopIteration
Python utilise les objets itérables dans différentes contextes, le plus important étant le for,
dans l'instruction for X in in Y , ou Y doit être un itérateur ou un objets sur lequel iter()
peut créer un itérateur. On alors l'équivalence :
>>> obj = (1,2,3,4)
>>> for item in obj :
print (item, end = " ")
1 2 3 4
>>> for item in iter (obj) :
print (item, end = " ")
1 2 3 4
Les itérateurs peuvent être matérialisés comme liste ou tuples en utilisant les constructeurs
list() ou tuple().
>>> L = [1, 2, 3]
>>> iterateur = iter(L)
>>> iterateur
<list_iterator object at 0x7fbd29398dd0>
>>> t = tuple (iterateur)
>>> t
(1, 2, 3)
Les séquences déballées peuvent aussi supporter les itérateurs : si vous connaissez un itérateur
qui retourne N éléments, vous pouvez le déballer dans un tuple à N éléments :
>>>
>>>
>>>
>>>
(1,
L = [1, 2, 3]
iterateur = iter (L)
a, b, c = iterateur
a, b, c
2, 3)
NDT : a, b, c = qqch construit le tuple (a, b, c) ; c'est l'emballage des tuples
Les fonctions prédéfinies comme max() ou min() peuvent comme argument un simple
itérateur et retourner le plus grand ou le plus petit élément. Les opérateurs in et not in
supportent aussi les itérateurs : X in itérateur est vrai si X est trouvé dans le flux retourné
par l'itérateur. Vous ne pourrez pas les utiliser dans les question utilisant des itérateurs infinis ;
max() et min() ne retournent jamais, et si l'élément X n'apparaît pas dans le flux, les
adaptation de la doc Python
page 5
Programmation fonctionnelle en Python
opérateurs in et not in peuvent retourner n'importe quoi.
Noter que dans un itérateur, vous pourrez seulement aller en avant ; il n'y a aucun moyen e
revenir à l'élément précédent, de faire un reset sur l'itérateur ; mais le protocole itérateur
permet de faire des copies d'itérateurs. Les objets itérateurs peuvent optionnellement procurer
ces possibilités additionnelles ; mais le protocole opérateur ne spécifie que la méthode
__next__(). Les fonctions peuvent parfois consommer toutes les sorties de l'itérateur, et si
vous avez besoin de faire quelque chose de différent avec le même itérateur, il vous faudra
créer un nouvel itérateur.
Les types de données qui supportent les itérateurs
Nous avons déjà vu comment les listes et les tuples supportent les itérateurs. En fait, n'importe
quel type de séquence, comme les chaînes, supporterons automatiquement la création d'un
itérateur.
L'appel de iter() sur un dictionnaire retourne un itérateur qui boucle sur les clef du
dictionnaire.
>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,\
'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for clef in m :
print (clef, m[clef])
Nov
Sep
Dec
Jul
Jun
Jan
Oct
Aug
Feb
May
Mar
Apr
>>>
11
9
12
7
6
1
10
8
2
5
3
4
Noter que l'ordre est aléatoire, car il est basé sur l'ordre de hachage des objets dans le
dictionnaire.
Quand on applique iter() sur un dictionnaire, on boucle toujours sur les clefs, mais les
dictionnaires ont des méthodes qui retournent d'autres itérateurs. Si vous désirer itérer sur les
valeurs ou les paires clef/valeur, vous pouvez explicitement appeler les méthodes values()
ou items() qui fournissent l'itérateur approprié.
Le constructeur de dict() accepte un itérateur qui retourne une flux fini de tuple (clef,
valeur) :
>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> it = iter (L)
>>> dict (it)
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}
>>> it = iter (L)
>>> next (it)
('Italy', 'Rome')
>>> dict (it)
adaptation de la doc Python
page 6
Programmation fonctionnelle en Python
{'France': 'Paris', 'US': 'Washington DC'}
>>>
Les fichiers supportent l'itération lors de l'appel de la méthode readlines() jusqu'à ce qu'il
n'y ait plus de lignes dans le fichier. Ce qui signifie qu'on peut lire chacune de lignes d'un
fichier comme ceci :
>>> for ligne in fichier :
>>>
# faire quelque chose avec chaque ligne
Les ensembles (set) prennent leur contenu à partir d'un itérable et vous pouvez itérer sur les
éléments de l'ensemble :
>>> S = { 2, 3, 5, 7, 11, 13 }
>>> for item in S :
print (item, end=" / ")
>>> 2 / 3 / 5 / 7 / 11 / 13 /
Expressions génératrices et compréhension des listes
Deux opérations habituelles sur la sortie d'un itérateur sont :
1. réaliser des opérations sur chacun des éléments,
2. sélectionner un sous-ensemble d'éléments qui remplissent certaines conditions. Par
exemple une liste de chaînes étant donnée, vous pouvez souhaiter supprimer les
espaces de début et de fin ou extraire toutes les chaînes ayant une sous-chaîne donnée.
Les compréhensions de listes et expressions génératrices (en abrégé listcomps et genexprs )
constituent une notation concise pour de telles opérations, empruntés au langage de
programmation fonctionnelle Haskell. Vous pouvez enlever tous les espaces des chaînes d'un
flux de chaînes avec le code qui suit :
>>> liste_de_lignes = ["
ligne 1\n", "ligne2\n
", "ligne
3
\n"]
>>> liste_de_lignes
['
ligne 1\n', 'ligne2\n
', 'ligne
3
\n']
>>> # expression génératrice : retourne un itérateur
>>> iter_sans_espace = ( ligne.strip() for ligne in liste_de_lignes )
>>> iter_sans_espace
<generator object <genexpr> at 0x7f331ebc5cd0>
>>> # compréhension de liste : retourne une liste
>>> lignes_sans_espace = [ ligne.strip() for ligne in liste_de_lignes ]
>>> lignes_sans_espace
['ligne 1', 'ligne2', 'ligne
3']
Vous pouvez sélectionner certains éléments en ajoutant une condition if :
>>> lignes_sans_espace = [ ligne.strip() for ligne in liste_de_lignes if
ligne[0] == " "]
>>> lignes_sans_espace
['ligne 1']
>>>
Avec une compréhension de liste, vous fournissez une liste Python ; lignes_sans_espace est
adaptation de la doc Python
page 7
Programmation fonctionnelle en Python
une liste contenant le résultat, pas un itérateur. Les expressions génératrices retournent un
itérateur qui calcule les valeurs quand on en a besoin, et qui n'a pas besoin de matérialiser les
valeurs. Ce qui signifie que les compréhensions de liste ne sont pas utiles si vous travaillez
avec des itérateurs qui peuvent retourner un flux infini ou un très important volume de
données. Les expressions génératrices sont préférables dans ces situations.
Les expressions génératrices sont comprises entre des parenthèses ("()") et les
compréhensions de listes sont mises entre crochets ("[]"). Les expressions génératrices ont
la forme suivante :
( expression for expr in sequence1
if condition1
for expr2 in sequence2
if condition2
for expr3 in sequence3
if condition3
for exprN in sequenceN
if conditionN )
Pour les compréhensions de listes, les crochets remplacent les parenthèses. C'est la seule
différence.
Les éléments en sortie seront les valeurs successives de expression. Les clauses if sont toutes
optionnelles ; si elles sont présentes, expression est évalué et les résultats ajoutés au résultat si
la condition est vérifiée.
Les expressions génératrices doivent toujours être écrites entre parenthèses, mais les
parenthèses signalant un appel de fonction sont également comptées ; Si vous désirez créer un
itérateur qui doit être immédiatement passé à une fonction, vous pouvez écrire :
# sum(iterable[, start])
# additionne start et les items de iterable et retourne le total. start
par défaut est 0
obj_total = sum (obj.count for obj in list_all_objects ())
La clause for...in contient les séquences sur laquelle itérer. Les séquences n'ont pas à être
de même longueur, parce que ils seront itérés de gauche à droite, pas en parallèle. Pour
chaque élément de sequence1, sequence2 on boucle à partir du début. sequence3 est bouclé
pour chaque paire d'éléments de résultat, de séquence1 et sequence2.
En d'autres mots, une compréhension de liste ou une expression génératrice est équivalente au
code Python qui suit :
for expr1 in sequence1:
if not (condition1):
continue
# Skip this element
for expr2 in sequence2:
if not (condition2):
continue
# Skip this element
...
for exprN in sequenceN:
if not (conditionN):
continue
# Skip this element
# Output the value of
# the expression.
adaptation de la doc Python
page 8
Programmation fonctionnelle en Python
Ce qui signifie que quand il y a de multiples clauses for
in, sans clause if, la longueur de
la sortie résultante sera égale au produit des longueurs de toutes les séquences. Si vous avez
deux listes de longueur trois, la liste de sortie a neuf éléments de long.
>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x,y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1),
('c', 2), ('c', 3)]
>>>
Pour éviter d'introduire une ambiguïté dans la grammaire de Python, si expression crée un
tuple, il doit être entouré de parenthèse. La première compréhension de liste ci-dessous
provoque une erreur de syntaxe, alors que la seconde est correcte :
>>> [ x, y for x in seq1 for y in seq2]
SyntaxError: invalid syntax
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3), ('b', 1), ('b', 2), ('b', 3), ('c', 1),
('c', 2), ('c', 3)]
Générateurs
Les générateurs constituent une classe spéciale de fonctions qui simplifient la tache dans
l'écriture des itérateurs. Les fonctions régulières calculent une valeur et la retournent, mais les
générateurs retournent un itérateur qui retourne un flux de valeur.
Vous êtes sans doute familier avec la manière dont les appels de fonctions régulières
fonctionnent en Python ou en C. Quand vous appelez une fonction, elle donne un espace de
nom privé où ses variables locales sont créées. Quand la fonction rencontre une instruction
return, les variables locales sont détruites et la valeur est retournée à l'appelant. Un appel
ultérieur de la même fonction va créer un nouvel espace de nom privé et un tout neuf
ensemble de variables locales. Mais, est-ce que les variables locales sont complètement
perdues lorsque l'on sort d'une fonction ? Est-ce que l'on peut recommencer ultérieurement
une fonction qui a été quittée ? C'est ce que font les générateurs, qui peuvent être pensés
comme des fonctions que l'on peut relancer.
Voici un exemple simple de fonction génératrice :
def generate_ints(N):
...
for i in range(N):
...
yield i
N'importe quelle fonction contenant le mot clef yield est une fonction génératrice ; le mot
clef est détecté pat le compilateur de bytecode qui compile la fonction spéciale comme un
résultat.
Quand on appelle une fonction génératrice, elle ne retourne pas une simple valeur ; à la place
elle retourne un objet générateur qui supporte le protocole itérateur. En exécutant l'expression
yield, le générateur sort la valeur de i, à la façon d'une instruction return. La grande
différence entre yield et return est qu'en atteignant le yield, l'état d'exécution du
générateur est suspendu et les variables locales sont préservées. Au prochain appel de la
méthode __next__() du générateur, la fonction va reprendre.
Voici un exemple simple, le générateur generateurEntiers() :
adaptation de la doc Python
page 9
Programmation fonctionnelle en Python
>>> def generateurEntiers (n) :
for index in range (n) :
yield index
>>> gen = generateurEntiers (3)
>>> gen
<generator object generateurEntiers at 0x7f740c12e050>
>>> next (gen)
0
>>> next (gen)
1
>>> next (gen)
2
>>> next (gen)
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
next (gen)
StopIteration
>>>
Vous pouvez également écrire for index in generateurEntiers(5) ou
a,b,c = generateurEntiers (3)
Dans une fonction génératrice, return value est sémantiquement équivalent à raise
StopIteration (value). Si aucune valeur n'est retournée ou que le bas de la fonction est
atteint, la suite des valeurs se termine et le générateur ne peut plus retourner de nouvelles
valeurs.
Vous pouvez réaliser l'effet des générateurs manuellement en écrivant votre propre classe et
en enregistrant toutes les variables locale des générateurs comme variables d'instance. Par
exemple, retourner une liste d'entiers pourrait être fait en posant self.count à 0 et en ayant
la méthode __next__() qui incrémente self.count et la retourne. Néanmoins, pour un
générateur d'une certaine complexité, écire la classe correspondante pourrait être plus
beaucoup plus costaud.
L'ensemble de tests Lib-test/test_generators.py, inclus dans la librairie Python,
contient plusieurs exemples plus intéressants. Voici un générateur qui implémente dans un
ordre transversal un arbre en utilisant les générateurs récursivement :
# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
if t:
for x in inorder(t.left):
yield x
yield t.label
for x in inorder(t.right):
yield x
Deux autres exemples de test_generators.py produisent des solutions au problème des n
reines (placer n reines sur un échiquier nXn de telle façon qu'aucune reine ne menace l'autre)
et celui du cavalier (trouver un chemin pour un cavalier sur un échiquier nXn passe une fois
adaptation de la doc Python
page 10
Programmation fonctionnelle en Python
par chaque case et ne passe jamais deux fois par la même case).
Passer des valeurs à un générateur
Avant Python 2.4., seuls les générateurs produisent des sorties. Une fois que le code du
générateur a été invoqué pour créer un itérateur, il n'y avait aucun aucun moyen d'envoyer de
nouvelles informations aux fonctions lors de la reprise de l'exécution. Vous pouviez
contourner cette impossibilité en utilisant dans le générateur une variable globale ou un objet
mutable que l'appelant pouvait modifier, mais ces approches sont fautives.
Avec Python 2.5 apparaît une façon simple de passer une valeur à un générateur. yield
devient une expression, qui retourne une valeur pouvant être assignée à une variable ou tout
autre opération :
val = (yield i)
Je vous recommande de toujours parenthéser l'expression yield quand vous faites quelque
chose avec la valeur retournée, comme sur l'exemple ci-dessus. Les parenthèses ne sont pas
toujours indispensables, mais il est facile de toujours les ajouter plutôt que de devoir se
rappeler des cas où elles sont nécessaires.
La PEP 342 explique la règle exacte, qui sont qu'une expression yiels doit toujours être
parenthésée sauf quand elle se présente comme une expression de haut niveau dans la partie
droite d'une affectation. Ce qui signifie que l'on peut écrire val = yield i mais que l'on doit
utiliser des parenthèses quand il y a une opération comme dans val = (yield i) + 12
Les valeurs sont envoyées dans un générateur en appelant sa méthode send(value). Cette
méthode reprend le code du générateur et l'expression yield retourne la valeur spécifiée. Si la
méthode régulière __next__() est appelée, le yield retourne None.
Voici un test de compteur simple qui s'incrémente par unité et permet de changer le compteur
interne :
>>> def compteur (maximum) :
i = 0
while i < maximum :
val = (yield i)
print (val)
# si une valeur est fournie on change le compteur
if val is not None :
i = val
else :
i += 1
>>> it = compteur (10)
>>> next (it)
0
>>> it.__next__()
None
1
>>> next (it)
None
2
>>> it.send(8)
8
8
>>> next (it)
None
9
adaptation de la doc Python
page 11
Programmation fonctionnelle en Python
>>> next (it)
None
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
next (it)
StopIteration
>>>
Parce que yield retournera souvent None, vous devrez toujours le vérifier. N'utilisez pas cette
valeur dans des expressions, à moins que vous soyez sur que la méthode send() sera la seule
méthode utilisée pour relancer la fonction génératrice.
En plus de send(), il y a deux autres méthodes applicables aux générateurs :
* throw (type, value= None, traceback=None) est utilisé pour lever une
exception à l'intérieur du générateur ; l'exception est levée par l'expression yield où
l'exécution de générateur s'arrête.
* close() lève une exception GeneratorExit à l'intérieur du générateur pour
terminer l'itération. En recevant cette exception, le code du générateur peut lever soit
GeneratorExit soit StopIteration ; attraper l'exception et faire n'importe quoi
d'autre est illégal et engendrera un RuntimeError.close() qui sera aussi appelée par
le ramasse-miette de Python lorsque le générateur est soumis au ramasse-miette.
Si vous avez besoin de nettoyer quand un GeneratorExit se produit, Je suggère
d'utiliser un try :...finally : au lieu de capturer GeneratorExit.
L'effet cumulatif de ces changements et d'orienter les générateurs d'un producteur
d'information à sens unique vers un rapport à deux, avec un producteur et un consommateur.
Les générateurs se font aussi des coroutines, une forme plus générale de subroutines. Les
subroutines sont entrées en un point et sorties en un autre point (le début de fonction et
l'instruction return), mais les coroutines peuvent être entrées, sortie ou relancées en
beaucoup de points différents (les instruction yield)
Fonctions prédéfinies
Voici un examen un peu plus détaillé des fonctions prédéfinies souvent utilisées avec les
itérateurs.
map() et filter()
Deux fonctions prédéfinies de Python, map() et filter() doublent les caractéristiques des
expressions génératrices.
map (f, iterA, iterB, . . . ) retourne un itérateur sur la séquence.
f (iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....
>>> def upper (s) :
return s.upper()
>>> list (map (upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper (s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']
>>>
Vous pouvez naturellement réaliser le même effet avec une compréhension de liste.
filter(predicate, iter) retourne un itérateur sur toute séquence d'élément qui remplit
adaptation de la doc Python
page 12
Programmation fonctionnelle en Python
certaines conditions, et il est de même dupliqué par les compréhensions de liste. Un predicat
est une fonction qui retourne la valeur de vérité d'un certaine condition ; pour être utilisé avec
filter(), le prédicat doit prendre une valeur simple.
>>> def est_pair (x) :
return (x % 2) == 0
>>>
[0,
>>>
>>>
[0,
>>>
list (filter(est_pair, range(20)))
2, 4, 6, 8, 10, 12, 14, 16, 18]
# avec une compréhension de liste
list (x for x in range(20) if est_pair(x))
2, 4, 6, 8, 10, 12, 14, 16, 18]
enumerate()
numérote les éléments de l'itérable et retourne des 2-tuples contenant le
numéro et chaque élément.
enumerate (iter)
>>> for item in enumerate(['pomme', 'poire', 'abricot']) :
print (item)
(0, 'pomme')
(1, 'poire')
(2, 'abricot')
>>>
est souvent utilisé quand on boucle sur une liste et qu'on enregistre les indices
des éléments pour lesquels une condition est vérifiée :
enumerate()
fichier = open('./montexte.txt', 'r')
for i, ligne in enumerate(fichier):
if ligne.strip() == "":
print("ligne vide en #%i" % i)
sorted()
collecte tous les éléments de l'itérable
dans une liste, trie la liste et retourne le résultat trié. Les paramètres key et reverse sont
passés pour renseigner la méthode de liste sort().
sorted (iterable, key=None, reverse=False)
>>> import random
>>> liste_aleatoire = random.sample(range(10000), 8)
>>> liste_aleatoire
[5441, 284, 706, 2580, 4580, 9237, 7192, 3087]
>>> sorted (liste_aleatoire)
[284, 706, 2580, 3087, 4580, 5441, 7192, 9237]
>>> sorted (liste_aleatoire, reverse=True)
[9237, 7192, 5441, 4580, 3087, 2580, 706, 284]
>>>
Pour plus de détails sur les tris, voir la section Sorting How To
any() et all()
Les fonctions prédéfinies any(iter) et all(iter) donnent la valeur de vérité de l'itérable
adaptation de la doc Python
page 13
Programmation fonctionnelle en Python
contenu comme argument. any() retourne True si un élément au moins de l'itérable est vrai,
et all() retourne True si tous les éléments de l'itérable sont vrais.
>>> any
True
>>> any
False
>>> all
True
>>> all
False
([0, False, 'toto'])
([0, None, False, [], ""])
(["0", 1, 'all', True, (1,)])
(["0", 1, 'all', True, []])
zip()
prend successivement les éléments de même rang de chaque
iterable pour en faire un tuple et le retourner.
zip(iterA, iterB, ...)
>>> it = zip ([1,2,3], ("a","b","c","d"))
>>> list(it)
[(1, 'a'), (2, 'b'), (3, 'c')]
>>>
La fonction ne construit pas dans la mémoire une liste pour garder tous les opérateurs d'entrée
avant de retourner, mais au contraire, les tuple sont construit et retournés uniquement si ils
sont requis (en termes techniques, ce comportement est l'évaluation paresseuse (lazy) dite
aussi retardée ou à la demande).
L'itérateur est supposé avoir des itérables de même longueur. Sinon c'est le plus court qui
impose le nombre de valeurs fournies. Vous devez éviter d'avoir des itérateurs de longueur
différente, parce qu'un éléments peut appartenir à un itérateur plus long, e t être écarté. Ce qui
signifie que vous ne pouvez pas utiliser des itérateurs plus importants, au risque de sauter des
éléments écartés.
Le module itertools
Le module itertools contient plusieurs itérateurs d'utilité courante e des fonctions qui
combinent plusieurs itérateurs. Cette section va introduire au contenu du module en donnant
quelques exemples.
Les fonction du module peuvent être classées :
* fonctions qui créent un nouvel itérateur à partir d'un itérateur existant.
* fonction pour traiter les éléments d'un itérateur comme arguments de fonction.
* les fonctions pour sélectionner les sorties d'un itérateur.
* une fonction pour grouper une sortie d'opérateur.
Créer de nouveaux itérateurs
itertools.count()
itertools.count(n)retourne un flux infini d'entiers, avec un incrément de 1 à chaque fois.
Vous pouvez optionnellement remplacer le nombre de départ, qui par défaut est 0.
itertools.count() =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
adaptation de la doc Python
page 14
Programmation fonctionnelle en Python
itertools.cycle()
sauvegarde une copie du contenu de l'itérable procuré et retourne
un nouvel itérateur qui retourne ses éléments depuis le premier jusqu'au dernier. Le nouvel
itérateur répète ces éléments indéfiniment.
itertools.cycle(iter)
itertools.cycle([1,2,3,4,5]) =>
1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
itertools.repeat()
itertools.repeat(elem, [n])
retourne l'élément fourni n fois, ou indéfiniment si n n'est
pas donné.
itertools.repeat('abc') =>
abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
abc, abc, abc, abc, abc
itertools.chain ()
prend un nombre arbitraire d'itérables en entrée, et
retourne tous les éléments du premier itérateur, puis tous les éléments du second, et ainsi de
suite jusqu'à épuisement des itérables.
itertools.chain(iterA, iterB, ...)
itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
a, b, c, 1, 2, 3
itertools.islice()
itertools.islice(iter, [start], stop, [step]) retourne un flox qui est une tranche
de l'itérateur. Avec uniquement l'argument stop, il retournera les premier stop éléments. Si
vous fournissez un index de départ, vous aurez les start-stop éléments, et si vous fournissez
un valeur pour step, les éléments seront donnés avec un saut de step éléments. À la différence
des chaînes Python et du découpage de liste, vous pouvez utiliser des valeurs négatives pour
start, stop et step.
itertools.islice(range(10), 8) =>
0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
2, 4, 6
itertools.tee ()
replique une itérateur, il retourne n itérateurs indépendants qui
tous retournent les contenus de l'itérateur source. Si vous ne fournissez pas de valeur pour n,
la valeur par défaut est 2. Répliquer les itérateurs requiert de sauvegarder un peu des contenus
de l'opérateur source, ce qui consomme significativement de la mémoire si l'itérateur et assez
important et qu'un des nouveaux itérateurs est utilisé plus que les autres.
itertools.tee(iter, [n])
itertools.tee( itertools.count() ) =>
iterA, iterB
adaptation de la doc Python
page 15
Programmation fonctionnelle en Python
avec iterA ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
et
iterB ->
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, …
Appels de fonctions sur des éléments
Le module operator contient un ensemble de fonctions correspondant aux opérateurs de
Python. Voici quelques exemples : operator.add(a, b) (additionne deux valeurs),
operator.ne(a, b) ( a != b), et operator.attrgetter('id') (retourne un callable qui
recherche l'attribut .id).
itertools.starmap(func, iter)
itertools.starmap(func, iter) assume que l'itérable retournera un retournera un flux de
tuple, et appelle func en utilisant ces tuples comme arguments.
itertools.starmap(os.path.join,
[('/bin', 'python'), ('/usr', 'bin', 'java'),
('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
/bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby
Sélectionner des éléments
Un autre groupe de fonctions choisit un sous ensemble dans les éléments d'un itérateur en se
basant sur un prédicat.
itertools.filterfalse()
itertools.filterfalse(predicate, iter)
est à l'opposé une fonction qui retourne les
éléments pour lequel le prédicat est faux.
itertools.filterfalse(is_even, itertools.count()) =>
1, 3, 5, 7, 9, 11, 13, 15, …
itertools.takewhile()
itertools.takewhile(predicate, iter) retourne les éléments aussi longtemps que
prédicat retourne True. Une fois que le prédicat retourne False, l'itérateur signale la fin
le
de
ses résultats.
def less_than_10(x):
return x < 10
itertools.takewhile(less_than_10, itertools.count()) =>
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
itertools.takewhile(is_even, itertools.count()) =>
0
itertools.dropwhile(predicate, iter)
itertools.dropwhile(predicate, iter) écarte les éléments tant que le prédicat retourne
True, et alors retourne le reste des résultats de l'itérable.
adaptation de la doc Python
page 16
Programmation fonctionnelle en Python
itertools.dropwhile(less_than_10, itertools.count()) =>
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.dropwhile(is_even, itertools.count()) =>
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...
Grouper des éléments
itertools.groupby(iter, key_func=None)
La dernière fonction que j'évoquerai, itertools.groupby(iter, key_func=None), est le
plus compliqué.
key_func(elem) est une fonction qui peut calculer une clef pour chaque élément retourné par
l'itérable. Si vous ne fournissez pas une tells fonction, la clef est simplement chaque élément
lui-même.
group_by() collecte tous les éléments consécutifs d'un itérable sous-jacent qui a les même
valeurs de clef et retourne un flux de tuple contenant une valeur de clef et un itérateur pour les
éléments possédant la clef.
city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
('Anchorage', 'AK'), ('Nome', 'AK'),
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
...
]
def get_state(city_state):
return city_state[1]
itertools.groupby(city_list, get_state) =>
('AL', iterator-1),
('AK', iterator-2),
('AZ', iterator-3), ...
où
iterator-1 =>
('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')
assume que les contenus l'itérable sous-jacent ont été trié selon les clefs. Noter
que les itérateurs retournés utilisent aussi l'itérables sous-jacent, et qu'ainsi, il faut épuiser
l'itérateur-1 avant de requérir l'itérateur-2 et ses clefs correspondantes.
groupby()
Le module functools
functools.partial()
Le module functools en Python 2.5 contient quelques fonctions de haut niveau. Une
fonction de haut niveau prend une ou plusieurs fonctions comme entrée et retourne une
nouvelle fonction. L'outil le plus utile de ce module est la fonction functools.partial().
Pour des programmes écrits en style fonctionnel, vous désirerez parfois construire des
variantes de fonctions existantes et qui incluent quelques paramètres. Considérons une
adaptation de la doc Python
page 17
Programmation fonctionnelle en Python
fonction Python f(a, b, c) ; vous pouvez souhaiter créer une nouvelle fonction g(b,c) qui
équivaut à f(1, b, c) ; vous incluez la valeur 1 des paramètres de f(). Ceci st appelé une
"application partielle de fonction".
Le constructeur de partial prend les arguments (function, arg1, arg2, ...,
kwarg1=value1, kwarg1=value2). Le résultat est callable, et ainsi, vous n'aurez qu'à
invoquer function avec ses arguments inclus.
Voici un exemple concret :
import functools
def log(message, subsystem):
"""Write the contents of 'message' to the specified subsystem."""
print('%s: %s' % (subsystem, message))
...
server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')
functools.reduce(func, iter, [initial_value]
functools.reduce(func, iter, [initial_value] cumule l'exécution d'une
opération sur tous les éléments de l'itérables et, par conséquent, ne peut être appliqué à un
itérable infini. func doit être une fonction qui prend deux éléments et retourne une valeur
simple. functools.reduce() prends les deux premiers éléments A et B retournés par l''itérateur
et calcule func(A, B). Elle requiert alors le troisième élément C, calcule func ((A,B,)C),
combine ce résultat avec le quatrième élément retourné et continue ainsi jusqu'à épuisement
de l'itérable. Si l'itérable ne retourne aucune valeur, une exception TypeError est levée. Si la
valeur initiale est fournie, elle est utilisée comme point de départ et func(initial_value, A) est
le premier calcul.
>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
...
TypeError: reduce() of empty sequence with no initial value
>>> functools.reduce(operator.mul, [1,2,3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1
Si vous utilisez operator.add() avec functools.reduce(), vous additionnerez tous
les éléments de l'itérable. Ce cas est si commun qu'il y a une fonction prédéfinie
spéciale appelée sum() pour faire ce calcul.
>>>
>>>
10
>>>
10
>>>
0
import functools
functools.reduce(operator.add, [1,2,3,4], 0)
sum([1,2,3,4])
sum([])
adaptation de la doc Python
page 18
Programmation fonctionnelle en Python
Pour beaucoup d'utilisation de la fonction reduce(), cependant, il peut être plus clair de se
contenter de faire une boucle for.
import functools
# Instead of:
product = functools.reduce(operator.mul, [1,2,3], 1)
# You can write:
product = 1
for i in [1,2,3]:
product *= i
Le module operator
Le module operator a déjà été mentionné. Il contient un ensemble de fonctions
correspondant aux opérateurs de Python. Ces fonctions sont souvent utilisée dans un code de
style fonctionnel parce qu'ils vous évitent d'écrire des fonctions triviales pour de simples
opérations. Voici quelques fonctions de ce module :
* opérateurs mathématiques : add(), sub(), mul(), floordiv(), abs(), ...
* opérations logiques : not_(), truth().
* opération bit à bit : and_(), or_(), invert().
* comparaisons :eq(), ne(), lt(), le(), gt(), and ge().
* identité d'objets : is_(), is_not().
La liste complète se trouve dans la documentation du module.
Petites fonctions et expression lambda
Quand vous écrivez un programme de style fonctionnel, vous avez souvent besoin de petites
fonctions qui agissent comme des prédicats ou combinent quelque éléments.
S'il y a des fonctions prédéfinies Python ou un module de fonction qui conviennent, vous
n'aurez aucunement besoin de définir de nouvelle fonctions :
stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)
Si la fonction dont vous avez besoin n'existe pas vous devez l'écrire.. Une façon d'écrire une
petite fonction est d'utiliser l'instruction lambda. L'expression lambda peut prendre prend
plusieurs paramètres qu'elle combine dans une expressions ; elle crée une fonction anonyme
qui retourne la valeur prise par l'expression.
>>> additionneur = lambda x, y : x + y
>>> additionneur (3, 4)
7
>>> print_affecter = lambda nom, valeur : nom + "=" + str(valeur)
>>> print_affecter ("xyz", 123/567)
'xyz=0.21693121693121692'
C'est une alternative à la méthode de l'intruction def qui sert habituellement à la définition
des fonctions :
def additionneur (x, y) :
adaptation de la doc Python
page 19
Programmation fonctionnelle en Python
return x + y
def print_affecter (nom, valeur) :
....return nom + "=" + str(valeur)
Laquelle des alternative est préférable ? C'est une question de style, ma tendance habituelle
étant d'éviter l'usage de lambda.
L'une des raisons de ma préférence est que lambda est très limitée dans les fonctions qu'elle
peut définir. Le résultat doit être calculable par une expression simple, ce qui signifie que
vous ne pouvez pas avoir une conditionnelle if... elif... else qui suppose plusieurs
comparaisons, ou une instruction try... except.. Si vous essayez d'en faire trop dans une
expression lambda, vous finirez avec une expression compliquée et difficilement lisible. Ainsi
que fait le code qui suit :
import functools
total = functtools.reduce (lambda a,b : (0, a[1]+b(1], items)[1]
Vous pouvez le faire, mais vous passerez du temps à désintriquer l'expression qui y figure.
Une courte expression def fait un peu mieux les choses :
import functools
def combine(a, b):
return 0, a[1] + b[1]
total = functools.reduce(combine, items)[1]
Mais c'est encore mieux si vous utilisez une boucle for :
total = 0
for a, b in items:
total += b
Ou encore la fonction prédéfinie sum() dans une expression génératrice :
total = sum(b for a,b in items)
Beaucoup d'utilisation de functools.reduce()sont plus claires que la boucle for.
Fredrik Lundh a suggérer l'ensemble de règles qui suivent pour refondre une fonction
lambda :
1. Écrire une lambda fonction.
2. Écrire un commentaire expliquant ce que peut bien faire la lambda fonction.
3. Étudiez le commentaire quelques temps, et trouvez un nom qui caractérise l'essence
du commentaire.
4. Convertir la lambda fonction et une instruction def en utilisant ce nom.
5. Enlever le commentaire.
J'aime vraiment ces règles, mais vous êtes libre de ne pas approuver le fait qu'il est mieux de
ne pas utiliser les lambda fonctions.
adaptation de la doc Python
page 20
Programmation fonctionnelle en Python
Téléchargement