• Aucun résultat trouvé

[PDF] Support de formation sur la programmation fonctionnelle python 3

N/A
N/A
Protected

Academic year: 2021

Partager "[PDF] Support de formation sur la programmation fonctionnelle python 3"

Copied!
20
0
0

Texte intégral

(1)

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

(2)

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

(3)

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

(4)

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__()

(5)

>>> 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 :

>>> L = [1, 2, 3]

>>> iterateur = iter (L) >>> a, b, c = iterateur >>> a, b, c

(1, 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 ;

(6)

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 11 Sep 9 Dec 12 Jul 7 Jun 6 Jan 1 Oct 10 Aug 8 Feb 2 May 5 Mar 3 Apr 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)

(7)

{'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']

>>>

(8)

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

(9)

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.

(10)

>>> 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

(11)

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

(12)

>>> 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.

(13)

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

>>> list (filter(est_pair, range(20))) [0, 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)) [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

>>>

enumerate()

enumerate (iter) numérote les éléments de l'itérable et retourne des 2-tuples contenant le

numéro et chaque élément.

>>> for item in enumerate(['pomme', 'poire', 'abricot']) : print (item)

(0, 'pomme') (1, 'poire') (2, 'abricot') >>>

enumerate() 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 :

fichier = open('./montexte.txt', 'r') for i, ligne in enumerate(fichier): if ligne.strip() == "":

print("ligne vide en #%i" % i)

sorted()

sorted (iterable, key=None, reverse=False) 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(). >>> 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()

(14)

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 ([0, False, 'toto'])

True

>>> any ([0, None, False, [], ""]) False

>>> all (["0", 1, 'all', True, (1,)]) True

>>> all (["0", 1, 'all', True, []]) False

zip()

zip(iterA, iterB, ...) prend successivement les éléments de même rang de chaque

iterable pour en faire un tuple et le retourner.

>>> 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) =>

(15)

itertools.cycle()

itertools.cycle(iter) 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([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 ()

itertools.chain(iterA, iterB, ...) 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(['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 ()

itertools.tee(iter, [n]) 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( itertools.count() ) => iterA, iterB

(16)

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 le

prédicat retourne True. Une fois que le prédicat retourne False, l'itérateur signale la fin 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.

(17)

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), ... iterator-1 =>

('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL') iterator-2 =>

('Anchorage', 'AK'), ('Nome', 'AK') iterator-3 =>

('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() 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.

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

(18)

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.

>>> import functools >>> functools.reduce(operator.add, [1,2,3,4], 0) 10 >>> sum([1,2,3,4]) 10 >>> sum([]) 0

(19)

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 :

(20)

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.

Références

Documents relatifs

L’expérience hallucinogène rituelle peut ainsi faire l’objet d’une description phénoménologique selon ces trois lignes directrices : il y a modification du

Depuis la rentrée 2001, les autori- tés catholiques mosellanes testent à leur tour de nouveaux programmes de « culture religieuse » dans l’enseigne- ment confessionnel

Érudit est un consortium interuniversitaire sans but lucratif composé de l’Université de Montréal, l’Université Laval et l’Université du Québec à Montréal. Il a pour mission

Il y a tout d’abord les cahiers qui lui servent de journal intime où consigner ses rêves, mais aussi de brouillons préparatoires pour ses graffiti et même de

D’autre part, dans l’ensemble du Kérala, du pays tamoul et au Sri Lanka, il est au cœur de l’histoire de Kannaki, mais sans être davantage qu’un élément narratif :

car, avec les fruits de quelques arbres de la for?t11, il constitue la principale source de prot?ines -, mais il tient une place fondamentale dans les

:فظنلأا جاتنلإا او لاعفلا ءادلأا ةينقتلا هذه دعت ةدعاقل لأا جنل ةيساس ةرادلإا ماظن حا ا اهفيرعت تم ثيح ،ةيئيبل ةدحتلما مملأل يئيبلا جمانبرلا لبق نم (

[r]