1-06. Programmation fonctionnelle
Un paradigme de programmation est une façon de concevoir l'organisation d'un programme informatique. Il existe trois principaux paradigmes de programmation : impératif, fonctionnel et objet. Dans ce chapitre, on va s’intéresser principalement à la programmation fonctionnelle.
Paradigmes de programmation
- Distinguer sur des exemples les paradigmes impératif, fonctionnel et objet.
- Choisir le paradigme de programmation selon le champ d’application d’un programme.
Un paradigme de programmation, c'est un modèle de pensée qui guide la façon dont on va :
- Structurer le code
- Organiser les données
- Décrire les processus de résolution de problèmes
Impératif
Le paradigme impératif est une approche de programmation qui décrit comment un programme doit s'exécuter à travers une séquence d'instructions qui modifient l'état du programme. Il repose sur quatre concepts fondamentaux :
- La séquence d'instructions qui s'exécutent l'une après l'autre
- L'affectation de variables (attribution de valeurs à des emplacements mémoire)
- Les structures conditionnelles (
if/else) pour créer des branchements - Les boucles (
while/for) pour répéter des instructions
Ce paradigme reflète directement le fonctionnement de l'architecture matérielle des ordinateurs et constitue le fondement sur lequel sont construits d'autres paradigmes comme l'orienté objet ou le fonctionnel. C’est celui que vous utilisez principalement depuis la classe de première.
Le paradigme impératif peut être comparé à une recette de cuisine avec des étapes précises à suivre dans l'ordre.
Objet
Ce paradigme organise le code autour d'objets qui combinent données et comportements :
- Les données sont regroupées dans des classes avec des attributs
- Les comportements sont définis par des méthodes
- On utilise des concepts comme l'encapsulation, l'héritage et le polymorphisme
On a déjà vu ça en détails dans le chapitre 1-03, je ne vais donc pas en dire plus ici.
Un programme écrit en orienté objet peut être comparé une entreprise avec différents services ayant chacun ses responsabilités et collaborant ensemble.
Fonctionnel
Ce paradigme, plus abstrait, s'inspire des mathématiques et se concentre sur les fonctions « pures » :
- Il évite au maximum les effets de bord (modification de données en dehors de la fonction)
- Les fonctions ne dépendent que de leurs paramètres d'entrée, pas de l'état global
- Une même entrée produit toujours la même sortie
- On privilégie l'immutabilité (on ne modifie pas les données, on crée de nouvelles valeurs)
C’est cette approche que nous allons détailler dans la suite de ce cours.
L’impératif est partout
Le paradigme impératif est omniprésent et fondamental dans la grande majorité des langages et des styles de programmation.
Même au sein d'autres paradigmes, l'impératif reste très présent :
- Dans les fonctions (même en programmation fonctionnelle), l'implémentation interne de ces fonctions utilise souvent des séquences d'opérations, des affectations temporaires, des boucles ou des conditions. L'objectif est d'isoler ces aspects impératifs à l'intérieur de la fonction pour qu'elle apparaisse « pure » de l'extérieur.
- Les méthodes d'objets manipulent les attributs de l'objet (ce qui est une forme d'affectation et donc d'impératif), effectuent des calculs séquentiels, des boucles, etc. L'approche objet structure le code et les données, mais les actions réalisées par les objets sont souvent de nature impérative.
La plupart des langages modernes (Python, Java, JavaScript, PHP, C#) sont multiparadigmes. Ils permettent d'écrire du code dans un style impératif, orienté objet, et de plus en plus, fonctionnel. Cependant, le socle de l'exécution reste souvent impératif. Les fonctionnalités des paradigmes fonctionnels ou objets sont construites sur cette base.
Programmation fonctionnelle
Prenons point par point les caractéristiques du paradigme fonctionnel et analysons-les :
- Il évite au maximum les effets de bord (modification de données en dehors de la fonction)
- Les fonctions ne dépendent que de leurs paramètres d'entrée, pas de l'état global
- Une même entrée produit toujours la même sortie
- On privilégie l'immutabilité (on ne modifie pas les données, on crée de nouvelles valeurs)
Les fonctions respectant ces exigences sont appelées des fonctions « pures »
Pas d’effets de bord
Soit le programme ci-dessous :
def fct():
global i
i = i*2
i = 5
fct()
fct n’est pas une fonction pure, car ce qu’elle modifie une donnée extérieure à celle-ci. i est une varibale global qui se trouvera affectée par fct. C’est donc un effet de bord.
Si notre objectif était de doubler la valeur de i à l’aide d’une fonction, il faudrait écrire :
def fct(i):
return i*2
i = 5
i = fct(i)
Paramètres d’entrée
Soit le programme ci-dessous :
def fct(): return i>5
i = 5
fct()
fct n’est pas une fonction pure, car ce qu’elle renvoie (True si i > 5 et False sinon) dépend de i. Et i n’est pas un paramètre d’entrée. C’est une variable global définie dans le scope du programme lui-même et donc exérieur à la fonction.
Pour la transformer en fonction pure, il faudrait écrire :
def fct(i): return i>5
i = 5
fct(i)
Mêmes entrées = même sortie
Ce point est une conséquence du point précédent. La fonction ne dépend de rien d’autre que des paramètres d’entrée. Tant qu’on lui donne les mêmes paramètres, elle produira toujours le même résultat, sans dépendre de l’état d’une variable globale.
Immutabilité des données
Une fonction pure ne modifie pas les données d’entrée. Elle renvoie de nouvelles données. Par exemple :
def fct(list, i):
list.append(i)
list = ["a", "b", "c"]
fct(list, "d") # list a été modifiée
fct n’est pas une fonction pure, car elle a modifié l’entrée list. Pour en faire une fonction pure, il faut qu’elle renvoie une nouvelle liste.
def fct(list, i):
return list + [i]
list = ["a", "b", "c"]
list2 = fct(list, "d") # ["a", "b", "c", "d"]
On voit ici que list n’a pas été modifiée. À la place, une nouvelle variable list2 a été créée, avec les éléments de list et "d".
Outils de programmation fonctionnelle
Fonctions d’ordre supérieur
Une fonction d’ordre supérieur est une fonction qui peut prendre, parmi ses paramètres d’entrée, une autre fonction. On va voir dans la suite de cette section deux exemples très utiles en Python : les fonctions filter et map.
Il est bien sûr possible de créer des fonctions d’ordre supérieur. N’oublier pas qu’une fonction n’est après tout qu’un type d’« objet » parmi tant d’autres…
def sum(a,b): return (a+b)
def diff(a,b): return (a-b)
def fos(f, a, b):
return f(a, b)
test1 = fos(sum, 2, 5) # test1 = 7
test2 = fos(diff, 2, 5) # test2 = -3
Fonctions ayant un nombre variable d’arguments
Imaginez que vous ayez besoin d’écrire une fonction qui renvoie la somme de tous ses arguments, peu importe leur nombre. La fonction ci-dessous pose problème car elle attend exactement trois arguments.
def sum(a, b, c): return a+b+c
Vous pourriez avoir besoin de lui en passer deux, ou bien dix…. Bien sûr, vous avez la possibilité de passer un unique argument qui est une liste.
def sum(list):
total = 0
for n in list:
total += n
return total
Arguments de type *args
La solution précédente marche très bien et est tout à fait correct. Mais il y a une autre possibilité : passer à la fonction un argument précédé d’une astérisque. Cette notation traitera tous les arguments que vous passez à la fonction comme un tableau.
def sum(*numbers):
total = 0
for n in numbers: # n successivement prend les valeurs dans *numbers
total += n
return total
print(sum(4,4,8,4)) # affiche 20
Attention cependant : des arguments multiples de type *args doivent nécessairement être passés après d’autres arguments éventuels pour des raisons évidentes de logiques.
def f(arg1, arg2, *args): ... # valide
def g(arg1, *args, arg2): ... # non valide
Arguments de type **kwargs
Lorsqu’on met deux astérisques devant les arguments multiples, Python attend des arguments de type a=…, b=… etc.
On appelle cela des arguments nommés et ils seront traités comme un dictionnaire.
def f(**kwargs): print(kwargs)
f(a=1, b=2, nom="Toto") # affiche {'a': 1, 'b': 2, 'nom': 'Toto'}
Il est possible d’avoir une fonction prenant à la fois des arguments de type *args et des arguments de type **kwargs. En effet, Python peut faire la différence entre des arguments simples et des arguments nommés.
Fonction filter
Soit le programme ci-dessous :
a = [ 1, 3, 5, 7 ]
def fct(x): return x >= 5
test = filter( fct, a )
test = list(test) # conversion en list
print(test) # affiche [5, 7]
On a passé à la fonction filter deux arguments : la fonction fct ainsi que a.filter filtre les éléments de a (qui doit être un itérable – liste ou dictionnaire) pour lesquels fct renvoie False et renvoie un filter object qui doit être converti en list.
Fonction map
Soit le programme ci-dessous :
a = [ 1, 3, 5, 7 ]
def fct(x): return x**2
test = map( fct, a )
test = list(test) # conversion en list
print(test) # affiche [1, 9, 25, 49]
On a passé à la fonction map deux arguments : la fonction fct ainsi que a. map applique la fonction fct aux éléments de a (qui doit être un itérable) et renvoie un map object qui doit être converti en list.
Fonctions anonymes
Il peut arriver (comme dans les exemples ci-dessus) qu’on ait besoin d’une fonction simple et une seule fois. Pour ne pas polluer son code avec la définition de ce genre de fonction, on peut utiliser une fonction anonyme. La syntaxe est la suivante :
lambda arguments : traitement des arguments (renvoie le résultat automatiquement)
lambda a,b : a+b # renvoie la somme a+b
Ces fonctions sont parfois utilisées comme argument de fonction d’ordre supérieur. Reprenons les deux exemples donnés pour filter et map en utilisant des fonctions anonymes :
liste = [ 1, 3, 5, 7 ]
test1 = filter( lambda x : x>=5, liste )
test2 = map( lambda x : x**2, liste )
L’écriture est plus concise.
Tri d’une liste de dictionnaire
En respectant le paradigme fonctionnel et en utilisant la fonction filter et une fonction anonyme, filtrer le tableau ci-dessous de manière à ne garder que les articles dont le prix est inférieur à 500 €.
articles = [
{ "name": "Ordinateur portable ZenAir", "prix": 1199, "ref": "48d5c19a1" },
{ "name": "Écouteurs Airdaube", "prix": 349, "ref": "05ccf48b7" },
{ "name": "Téléphone Samsonne", "prix": 699, "ref": "fc4804c2" },
{ "name": "Tablette iPay Pro", "prix": 2499, "ref": "a73e21fc8" },
{ "name": "Montre connectée iWatchU", "prix": 849, "ref": "2b67f4d3e" },
{ "name": "Casque audio Bats Studio", "prix": 299, "ref": "9c24e8a5b" },
{ "name": "Enceinte intelligente Alexoom", "prix": 129, "ref": "6f12d7c9a" },
{ "name": "Télévision Somy", "prix": 899, "ref": "3e8b5f2d1" },
{ "name": "Console GameStation Macrohard", "prix": 459, "ref": "7d4c09e8f" },
{ "name": "Clavier Pov Pomme", "prix": 499, "ref": "5a3b2e7d1" },
{ "name": "Station d'accueil iDock Pample", "prix": 149, "ref": "1c8e4a7b3" },
{ "name": "Projecteur portable Booble Cast", "prix": 279, "ref": "8e1d5f3a2" }
]
Correction
test = filter(lambda x : x["prix"] < 500, articles)
test = list(test)