Synthèse Elève 2022-2023 V2

Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1sur 6

Programmation fonctionnelle

Paradigme de programmation

I) Programmation fonctionnelle :

A) Introduction :
Écrire des fonctions, c’est le lot quotidien de tous les développeurs informatiques : tous codent
régulièrement ces « briques élémentaires » qui composent les logiciels de tous les jours. Mais une brique
mal conçue peut fragiliser tout l’édifice, et personne ne souhaite qu’il finisse en champ de ruine !

La conception de ces briques repose sur un ensemble de techniques rassemblées sous le nom
de programmation fonctionnelle. Ce paradigme de programmation, utilisé par OCaml, Erlang, F# et
autres, a été largement ignoré par les professionnels durant de très nombreuses années, restant cantonné
aux sphères universitaires et au petit monde de la recherche. Mais cette ère est révolue : la programmation
fonctionnelle (PF ou FP, abréviation de Functional Programming en anglais) est possible en Python
(ainsi que dans un grand nombre d’autres langages).

Nous allons voir ce que cela signifie exactement, comment cela fonctionne et ce que cela implique.

Dans la réalité, tout langage de programmation doit être fonctionnel, ne serait-ce qu’un peu : on
crée des fonctions dans quasiment tous les langages, et ce depuis les débuts de l’informatique.
Qualifier un langage de fonctionnel implique qu’il réponde correctement aux définitions suivantes :
 il est possible d’utiliser une fonction comme paramètre d’une autre fonction ;
 il est possible de retourner une fonction comme résultat d’une autre fonction ;
 il est possible d’assigner une fonction à une variable ;
 il est possible de stocker une fonction dans une structure de données.
Si ces conditions sont respectées, on dit alors que les fonctions dans ce langage sont des fonctions de
première classe, et que le langage est fonctionnel.

Exemples :

 Utiliser une fonction comme paramètre d’une autre fonction :


Écrire en Python une fonction carre prenant en argument un flottant et renvoyant son carré
Écrire en Python une fonction cube prenant en argument un flottant et renvoyant son cube
Écrire une fonction calcul prenant en argument une fonction et un flottant et renvoyant l’image de ce
flottant par la fonction.

 Retourner une fonction comme résultat d’une autre fonction, assigner une fonction à une variable,
stocker une fonction dans une structure de données :
- Écrire en python 5 fonctions : bing(), boom(), bang,() paf(), kaboom() affichant
respectivement : "Bing !", "Boom !", "Bang !", "Paf !", "Kaboom !"
- Stocker ces fonctions dans une structure de données bruitages de type list.
- Écrire une fonction choixaléa() renvoyant aléatoirement une de ces fonctions
- Écrire une structure permettant de choisir 10 fois aléatoirement une de ces fonctions, de l’assigner à la
variable choix, et de retourner le résultat de cette fonction.
Remarque :
On peut ainsi appliquer n’importe quelle fonction sur tous les éléments d’un objet de type list (en
Python). Que fait le script ci-dessous ?
def tableauAuCarre(tab):
for i in range(len(tab)):
tab[i] = tab[i] ** 2

tableau = [-1, 1, 3.14]


tableauAuCarre(tableau)
print(tableau)
Quel(s) problème(s) ce script engendre-t-il ?
……………………………………………………………………………………
……………………………………………………………………………………
……………………………………………………………………………………

La programmation fonctionnelle apporte une solution « élégante » à ce problème : elle nous permet de
créer une fonction qui parcourt l’ensemble des éléments d’un tableau (on dit aussi qu’elle « itère sur un
tableau »), et qui applique alors le traitement souhaité à chacun des éléments du tableau.

Exemple :
- Reprendre les fonctions carre, cube
- Écrire une fonction nommée fonctionTableau prenant comme argument une liste de flottants, et une
fonction, qui modifie en place chaque élément selon la fonction envoyée en argument.
- exécuter la fonction fonctionTableau avec la fonction carre puis la fonction cube et afficher le
résultat

Définition
La technique consistant à passer une fonction (réalisant un traitement) en paramètre d’une autre fonction
(ce qui permet de changer de traitement à volonté), s’appelle la généralisation.

Plus de productivité !
L’approche fonctionnelle permet souvent de réduire le nombre de lignes, ce qui rend le code plus
lisible, tout en laissant plus apparent et compréhensible l’algorithme sous-jacent. Nous allons voir quels
outils sont mis à notre disposition pour parvenir à ce petit miracle...
Souvent, les langages implémentant (certaines caractéristiques de) la programmation fonctionnelle
fournissent par défaut plusieurs « fonctions permettant d’appliquer des fonctions ».
Dans la suite, on se concentre sur les fonctions agissant sur des tableaux (des list Python), car il s’agit
certainement des exemples les plus intéressants - et surtout les plus simples et donc accessibles.

En Python, la programmation fonctionnelle repose sur un type d’objet plus général appelé itérateur.
Les listes Python (objets de type list) mais aussi les dictionnaires (objets de type dict) sont nativement
des itérateurs.

Notion d’itérateur
Un itérateur Python est un objet qui représente un flux de données. Il est conçu pour renvoyer
chaque donnée du flux une par une (un élément à la fois).

Lorsqu’on définit un itérateur, on doit implémenter une méthode nommée __next__(), qui ne prend
pas d’argument et renvoie toujours l’élément suivant du flux. S’il n’y a plus d’élément dans le flux,
__next__() doit lever une exception StopIteration (ce n’est toutefois pas indispensable, il est
envisageable d’écrire un itérateur qui produit un flux infini de données).
La primitive iter() (une primitive est une fonction native de Python) prend un objet arbitraire et tente
de construire un itérateur qui renvoie le contenu de l’objet (ou ses éléments) en levant une exception
TypeError si l’objet ne gère pas l’itération. Plusieurs types de données natifs à Python gèrent
l’itération, notamment les listes et les dictionnaires.
On appelle itérable un objet pour lequel il est possible de construire un itérateur.
Remarque :
En programmation fonctionnelle, il n’y a que des évaluations de fonctions, et l’évaluation d’une
fonction ne dépend que de la valeur de ses paramètres, et pas de facteurs externes, ce qui exclut les
changements d’état qui pourraient provoquer des « effets de bord » (c'est-à-dire la modification d’une
variable non locale, ou un argument mutable passé par référence).
Les « variables » sont donc constantes, on ne modifie pas leurs valeurs, mais on peut créer de nouveaux
espaces mémoires par appels récursifs.
Il n’y a donc pas de compteur, et pas de boucles. Elles sont remplacées par les appels récursifs de
fonctions.

Exemple :
En quoi la fonction suivante est-elle impure ? (c'est-à-dire produisant des effets externes).
def f(x) :
a[0] += 1
return a[0]
a = [0]
print(f())
……………………………………………………………………………………
……………………………………………………………………………………
……………………………………………………………………………………
Remarque :
Un code sans effet de bord est plus fiable, plus facile à maintenir, à tester, à réutiliser.

Supplément : la fonction lamba », ou « fonction anonyme »


Lorsqu’on a besoin d’une fonction « one shot », qu’on ne compte pas réutiliser et dont le nom a alors peu
d’importance, de nombreux langages offrent la possibilité de simplifier la définition des fonctions. Ainsi,
Python offre la notion de fonction lambda :
 le mot-clé est lambda ;
 on ne donne aucun nom à la fonction ;
 le(s) argument(s) ne sont pas entourés de parenthèses et suive(nt) le mot-clé lambda ;
 après le « : » qui sépare la « définition » de la fonction (lambda) de son « corps », on indique ce qu’elle
doit renvoyer comme résultat sans le mot-clé return.

Exemple
Une façon non orthodoxe de définir une « fonction distance » serait :
distance = lambda x, y: (x**2+y**2)**.5
distance(3,4)
Que donne ce code ? …………………………………………………………………

B) Exemples de fonctions permettant d’appliquer des fonctions :

 La fonction map()
Il s’agit d’une des opérations de base, permettant à partir d’un objet de type list (par exemple),
de créer un nouvel objet (l’objet d’origine n’est donc pas modifié), dont chaque élément s’est vu
appliquer la fonction passée en paramètre à map()).
Exemple :
t1 = [-1, 1, 3.14]
map(lambda x : x**2, t1)

t2 = list(map(lambda x : x**2, t1))

for i in map(lambda x: x**2, t1):


print(i)
 La fonction reduce()
reduce() permet de réduire un tableau à une donnée.

Exemple :
from typing import List

def Sans_reduce(func : callable, tab : List[float]) -> float:


resultat = 0
for i in range(len(tab)):
resultat = func(resultat, tab[i])
return resultat

def somme(a : float, b : float) -> float:


return a + b

print(Sans_reduce(somme, [1, 2, 3, 4]))


from functools import reduce # Indispensable

print(reduce(lambda x,y : x+y, [1, 2, 3, 4]))


Testez les codes précédents, que renvoient-ils ?
…………………………………………………………………………………
…………………………………………………………………………………
…………………………………………………………………………………
…………………………………………………………………………………
Exemple :
Que donne le code suivant (essayez d’anticiper le résultat avant de le saisir !) ?
print(reduce(lambda x,y : x*y, [1, 2, 3, 4]))

 La fonction filter()
Pour ne conserver que certains éléments d’une structure de données répondant à une condition
spécifique on utilise la fonction filter(), qui va filtrer les éléments (d’un tableau, par exemple)
pour conserver uniquement ceux répondant à une condition, que l’on nomme le postulat. Au final, cette
fonction fabrique un nouveau tableau contenant uniquement les éléments voulus.

Exemple
from typing import List

def Sans_filter(func : callable, tab : List[float]) -> List[float]:


resultat = []
for elt in tab:
if func(elt):
resultat.append(elt)
return resultat

def positif(x : float) -> bool:


return x >= 0

print(Sans_filter(positif, [1, -2, 3, -4]))

print(list(filter(lambda x : x >=0 , [1, -2, 3, -4])))

Testez les codes précédents, que renvoient-ils ? …………………………………………


…………………………………………………………………………………
Compléments
En Python d’autres fonctions, d’autres possibilités, qui relèvent de l’approche fonctionnelle :
 La primitive : sorted(iterable, key_function) : permet de trier un itérable (un tableau,
par exemple), une fonction key_function permettant d’affiner la façon dont on trie.
 Le mot-clé : in (une primitive du langage aussi) : permet de tester la présence d’un élément dans un
objet de type list ou tuple, mais aussi dans les clés d’un dictionnaire il permet également de savoir si une
sous-chaîne de caractères est présente dans une chaîne :

 Les compréhensions de listes et de dictionnaires


Voici un exemple un peu complexe de compréhension de dictionnaire :
{j:"pair" if j%2==0 else "impair" for j in [(i+1)**2 for i in
range(10)]}

Pour plus de lisibilité, on aurait pu écrire aussi (en ajoutant des parenthèses pour mieux délimiter les
valeurs) :
{j:("pair" if j%2==0 else "impair") for j in [(i+1)**2 for i in
range(10)]}

C) Avantages / inconvénients : Impact sur la qualité du code

La programmation fonctionnelle est bénéfique pour la qualité du code, mais seulement si elle respecte
certaines règles. Par exemple :
 toute fonction complexe doit être déclarée et nommée (pour faire simple, au delà de 2 lignes de
code, nommez vos fonctions ! et naturellement, nommez-les si vous devez les réutiliser : dupliquer du
code n’est jamais une bonne idée) ;
 si une fonction contient un enchaînement de fonctions (ou de méthodes) un peu long et / ou complexe,
il est préférable de déclarer et nommer ladite fonction ;
 il est plus que souhaitable d’éviter les effets de bord sur les fonctions utilisées dans les map(),
reduce(), etc. En clair, cela signifie qu’il faut éviter de modifier la structure et / ou les données sur
lesquelles agissent vos fonctions (remarquez que map() et filter() fonctionnent ainsi : elles ne
modifient pas leurs « arguments », mais renvoient une nouvelle structure).
Les fonctions qui ne modifient pas les données sur lesquelles elles travaillent sont appelées fonctions
pures. C’est vraiment là l’idée forte de la programmation fonctionnelle : séparer les données qui, une
fois référencées par des variables, n’évoluent plus (sauf à créer de nouvelles variables), des traitements
(qui sont réalisés par les fonctions, agissant sur les données sans modifier les structures existantes, mais
au contraire en créant de nouvelles structures).
Ces différentes règles permettent de tirer le meilleur de la programmation fonctionnelle et de conserver
un code « propre ».

 Lisibilité

L’un des principaux avantages de la programmation fonctionnelle est la lisibilité. Elle nécessite des pré-
requis (la connaissance des fonctions de base) et un peu d’expérience, mais une fois cette connaissance
acquise, elle permet d’avoir un code plus simple, plus compréhensible et plus court :
 le nombre de boucles présentes dans un programme se trouve souvent considérablement réduit (elles
sont remplacées par des appels de fonctions) ;
 on peut travailler sur les éléments d’un « tableau » directement, sans recourir à la syntaxe tableau[i].
Pour remplacer l’imbrication des boucles (une boucle dans une boucle dans une boucle dans...), on peut
envisager de :
 créer une fonction permettant de traiter notre structure sur toutes les dimensions qu’elle nécessite
(ce qui nécessite de coder une fonction par type d’itération - c’est donc rapidement fastidieux) ;
 déclarer plusieurs fonctions différentes (une par boucle), et chaque fonction va appeler une autre
fonction au moment où la boucle est nécessaire (cette approche permet de séparer l’enchaînement des
boucles, mais peut rendre la lecture un peu plus délicate).
 Maintenabilité et évolutivité

Une des conséquences directes d’une meilleure lisibilité est d’avoir un code beaucoup plus
maintenable. Cette maintenabilité vient aussi du fait qu’il est possible de déboguer chaque étape d’un
enchaînement de fonctions assez facilement, contrairement à une écriture impérative dont les différents
traitements seraient mélangés. La séparation du traitement et de l’itération permet aussi de faciliter
l’identification des erreurs, en proposant notamment de tester la fonction de traitement indépendamment
du reste.
L’évolutivité est aussi plutôt avantagée : il est beaucoup plus simple de rajouter une nouvelle étape dans
enchaînement que dans une boucle classique.

 Testabilité

Sur ce point, la programmation fonctionnelle est encore plus intéressante : chacun de nos
traitements importants peut être transformé en une suite de fonctions. Il devient donc possible de tester
chacune de ces fonctions complètement, et indépendamment les unes des autres.

Vous aimerez peut-être aussi