Aller au contenu

Paradigmes de programmation

I. Les différents paradigmes⚓︎

Il existe plusieurs façons de résoudre un problème à l'aide d'un langage de programmation. Une façon d'approcher un problème correspond à un style de programmation qu'on qualifie de paradigme. La plupart des langages de programmation généralistes modernes permettent d'utiliser plusieurs paradigmes et de les mélanger dans un même programme. On parle de langages multi-paradigmes.

On va présenter quelques paradigmes parmi les plus répandus.

🖽 Paradigme impératif⚓︎

Des notions familières

La programmation impérative repose sur des notions qui vous sont familières :

  • la séquence d'instructions (les instructions d'un programme s'exécutent l'une après l'autre)
  • l'affectation (on attribue une valeur à une variable, par exemple : a = 5)
  • l'instruction conditionnelle (if / else)
  • la boucle (while et for)

Le paradigme impératif

Dans le paradigme impératif, les données sont stockées dans des variables et le programme s'organise comme une séquence d'instructions qui vont modifier l'état du programme (données en mémoire et position dans le code source) depuis un état initial jusqu'à un état final correspondant à la solution du problème.

Quelques traits principaux du paradigme impératif

  • La valeur d'une variable peut évoluer au cours de l'exécution : on parle de structure mutable
  • Une instruction effectue une action pouvant modifier l'état du programme : ce peut être une affectation de variable (modification des données en mémoire), une structure de contrôle (test ou boucle qui modifie la position dans le code source)
  • Un programme est une séquence d'instructions.
  • Les unités de code réutilisables peuvent être stockées dans des fonctions ce qui facilite la lisibilité, la maintenance, la réutilisabilité. On parle alors de programmation structurée.

🖽 Paradigme objet⚓︎

En bref

  • Le paradigme objet organise les données en une collection d'objets dont l'état interne (stocké dans des attributs) peut être modifié à l'aide de méthodes (des fonctions).
  • Les objets sont instanciés à partir de classes qui étendent la notion de type du paradigme impératif.
  • Un programme se présente comme une séquence d'interactions entre objets.
  • Les objets sont souvent des structures mutables et le paradigme objet est une sorte de surcouche du paradigme impératif dont il reprend les concepts de variable, de séquence et de structure contrôle.
  • La plupart des langages modernes comme Python, supportent ces deux paradigmes.

💡 A noter

Le paradigme objet permet de représenter des structures de données complexes en garantissant une propriété d'encapsulation

  • l'utilisateur ne peut manipuler la structure qu'à travers une interface publique de façon indépendante de l'implémentation qui reste cachée et peut être modifiée sans impact sur le code client
  • l'encapsulation facilite le travail en équipe sur de gros projets en permettant le découpage d'un programme en modules indépendants

🖽 Paradigme fonctionnel⚓︎

📓 Présentation

Le paradigme fonctionnel organise un programme comme un enchaînement d'évaluations de fonctions, chaque résultat produit en sortie d'une fonction étant pris en entrée de la fonction suivante.

Caractéristiques

Il en découle un certain nombre de traits spécifiques au paradigme fonctionnel :

  • La valeur d'une variable ne change pas. Les structures de données sont immuables c'est-à-dire qu'elles ne peuvent être modifiées après leur création. Cela permet d'empêcher les effets de bord.
  • Il n'existe donc pas d'instructions comme l'affectation qui peuvent modifier l'état du programme. Le calcul repose sur l'évaluation d'expressions, qui ont une valeur, et de fonctions, qui associent à une valeur, une autre valeur.
  • Les fonctions sont des valeurs commes autres. Une fonction peut être argument d'une autre fonction, valeur de retour d'une autre fonction, stockée dans une structure de données.
  • Une fonction peut donc s'appliquer à d'autres fonctions, on parle de fonction d'ordre supérieur : les analogies mathématiques sont la composition de fonction, la dérivation, l'intégration ...
  • Les structures d'itération commes les boucles du paradigme impératif sont remplacées par la récursion.
  • Les fonctions sont des fonctions pures c'est-à-dire qu'elles ne provoquent pas d'effets de bord lors de leur évaluation et que pour des entrées fixées, elles donnent toujours le même résultat en sortie. Cette propriété garantit la transparence référentielle c'est-à-dire que tout appel de fonction peut être remplacé par la valeur de son évaluation sans modifier le programme. Ceci ne serait pas garanti avec une fonction impure dont l'évaluation pourrait s'accompagner d'effets de bord en plus du calcul du résultat.

II. Le paradigme fonctionnel⚓︎

1. Exemple 1 : effet de bord⚓︎

Exemple 1

Exécuter le code ci-dessous. Que se passe-t-il ?

###
def ajoutpy-und1(mapy-undliste, i):bksl-nl mapy-undliste.append(i)bksl-nlbksl-nldef ajoutpy-und2(mapy-undliste, i):bksl-nl return mapy-undliste + [i]bksl-nlbksl-nlunepy-undliste = [4, 7, 3]bksl-nlprint("Avant appel de la fonction ajoutpy-und1 unepy-undliste = ", unepy-undliste)bksl-nlajoutpy-und1(unepy-undliste, 6)bksl-nlprint("Après appel de la fonction ajoutpy-und1 unepy-undliste = ", unepy-undliste)bksl-nlbksl-nl# Idem avec l'autre fonctionbksl-nlbksl-nlunepy-undliste = [4, 7, 3]bksl-nlprint("Avant appel de la fonction ajoutpy-und2 unepy-undliste = ", unepy-undliste)bksl-nlnouvellepy-undliste = ajoutpy-und2(unepy-undliste, 6)bksl-nlprint("Après appel de la fonction ajoutpy-und2 unepy-undliste = ", unepy-undliste)bksl-nlprint("Nouvelle liste créée : ", nouvellepy-undliste)bksl-nlbksl-nl



Solution
  • La fonction ajout_1 ne respecte pas le paradigme fonctionnel, car nous avons un effet de bord (la variable une_liste est modifiée par la fonction ajout_1).
  • La fonction ajout_2 ne modifie aucune variable, elle crée un nouveau tableau. Elle ne produit pas d'effet de bord.

2. Exemple 2 : Transparence référentielle⚓︎

Exemple 2

Exécuter le code ci-dessous. Que se passe-t-il ?

###
def incrementepy-und1(k):bksl-nl global nbksl-nl n = n + kbksl-nl return nbksl-nlbksl-nldef incrementepy-und2(n, k):bksl-nl n = n + kbksl-nl return nbksl-nlbksl-nln = 2bksl-nlbksl-nlfor i in range(3):bksl-nl print ("L'appel incrementepy-und1(3) donne : ", incrementepy-und1(3))bksl-nlbksl-nlfor i in range(3):bksl-nl print ("L'appel incrementepy-und2(2, 3) donne : ", incrementepy-und2(2, 3))bksl-nlbksl-nl



Solution

Les langages fonctionnels ont comme autre propriété la transparence référentielle. Ce terme recouvre le principe simple selon lequel le résultat du programme ne change pas si on remplace une expression par une expression de valeur égale. Ce principe est violé dans le cas de procédures à effets de bord puisqu'une telle procédure, ne dépendant pas uniquement de ses arguments d'entrée, ne se comporte pas forcément de façon identique à deux instants donnés du programme.

Ici, la fonction incremente_1 ne respecte donc pas cette proporiété de transparence référentielle. Elle ne respecte pas le paradigme fonctionnel

Les fonctions pures⚓︎

Les fonctions pures

  • Une fonction pure est une fonction qui ne modifie rien ; elle ne fait que renvoyer des valeurs en fonction de ses paramètres.
  • Les modifications qu’une fonction peut effectuer sur l’état du système sont appelées effets de bord. Un affichage à l’écran est un exemple d’effet de bord.

Fonctions d'ordre supérieur⚓︎

Des fonctions passées en paramètres

Les fonctions sont des objets de première classe, ce qui signifie qu'elles sont manipulables aussi simplement que les types de base.

👉 Une fonction peut prendre des fonctions comme paramètres ou renvoyer une fonction comme résultat.

Exemple 3

Exécuter le code ci-dessous. Que se passe-t-il ?

###
def clefpy-undnote(monpy-undtuple):bksl-nl return (monpy-undtuple[0])bksl-nlbksl-nldef clefpy-undnom(monpy-undtuple):bksl-nl return (monpy-undtuple[1])bksl-nlbksl-nltab = [(16, 'MARIE') , (16, 'ISMAEL'), (12, 'ANNE'), (17, 'SARAH')] bksl-nlprint("Par ordre décroissant des notes : ", sorted(tab, key=clefpy-undnote, reverse=True))bksl-nlprint("Par ordre alphabétique des noms : ", sorted(tab, key=clefpy-undnom, reverse=False))bksl-nlbksl-nl



Solution

La fonction sorted est une fonction d'ordre supérieur, qui prend en paramètre une fonction, comme ici clef_note ou clef_nom

Fonctions anonymes et opérateur lambda

On peut écrire le même code de façon plus concise, en utilisant des fonctions anonymes, grâce à l'opérateur lambda.

Par exemple la fonction :

🐍 Script Python
def double(x):
    return 2 * x

Peut être remplacée par

🐍 Script Python
lambda x: 2 * x

On a "perdu" le nom de cette fonction, qui parfois n'est pas utile (d'où le nom de fonction anonyme)

Si on le désire, on peut écrire :

🐍 Script Python
double = lambda x: 2 * x
tester ci-dessous :

###
tab = [(16, 'MARIE') , (16, 'ISMAEL'), (12, 'ANNE'), (17, 'SARAH')] bksl-nlprint("Par ordre décroissant des notes : ", sorted(tab, key=lambda monpy-undtuple:monpy-undtuple[0], reverse=True))bksl-nlprint("Par ordre alphabétique des noms : ", sorted(tab, key=lambda monpy-undtuple:monpy-undtuple[1], reverse=False))bksl-nlbksl-nl



Deux fonctions en paramètres

Exécuter le code ci-dessous, observer le résultat.

Vous pouvez expérimenter en mettant vos propres fonctions.

###
def sommepy-undfonctions(f, g, x):bksl-nl return f(x) + g(x)bksl-nlbksl-nldef f1(x):bksl-nl return xpy-strpy-str2bksl-nlbksl-nldef f2(x):bksl-nl return 2py-strxbksl-nlbksl-nlprint(sommepy-undfonctions(f1, f2, 5))bksl-nlbksl-nl



Renvoyer une fonction⚓︎

Une fonction qui prend deux fonctions en pramètres et renvoie une fonction

Dans l'exemple précédant le résultat renvoyé était un réel, calculé à l'aide des paramètres (f, g, x).

Nous allons maintenant créer une fonction qui prend en paramètres seulement des fonctions, et renvoie une fonction.

Tester ci-dessous

###
def sommepy-undfonctions(f, g):bksl-nl return lambda x: f(x) + g(x)bksl-nlbksl-nldef f1(x):bksl-nl return xpy-strpy-str2bksl-nlbksl-nldef f2(x):bksl-nl return 2py-strxbksl-nlbksl-nlk = sommepy-undfonctions(f1, f2) # k est une fonction !bksl-nlbksl-nlprint(k(5))bksl-nlbksl-nl



Solution

k est une fonction, et on peut l'appeler avec n'importe quel nombre en paramètre.

les fonctions affines

On peut également définir une fonction qui renvoie une fonction. La fonction affine prend en paramètres deux nombres a et b et renvoie la fonction \(x \mapsto ax + b\).

Exécuter le code ci-dessous, puis recopier ligne par ligne dans la console (exécuter chaque ligne):

Recopier
>>> f1 = affine(3, -2)
>>> f1(5)
>>> affine(-1, 4)(3)
###
affine = lambda a, b: lambda x: apy-strx + bbksl-nlbksl-nl



Qu'obtenez-vous ?

Solution
🐍 Console Python
>>> f1 = affine(3, -2)
>>> f1(5)
13
>>> affine(-1, 4)(3)
1

f1 est la fonction affine définie par : pour tout \(x\) on a \(f1(x)=3x-2\)
\(f1(5)=15-2=13\)
On a ensuite créé la fonction affine définie par : pour tout \(x\) on a \(f(x)=-x+4\) .
On a ensuite déterminé l'image de 1 par cette fonction : \(-3+4=1\)

Exercice sur les fonctions du second degré

Écrire le code de la fonction trinome qui prend en paramètre 3 nombres a, b et c, avec a non nul, et qui renvoie la fonction \(x \mapsto ax^2+ bx +c\).

Exemples d'utilisation
>>> f = trinome(1, 1, 1)  # x^2+x+1
>>> f(2)  # 2^2+2+1 = 7
7
>>> f(0)  # 0^2+0+1 = 1
1
>>> trinome(3, -1, 2)(6)  # 3*6^2-6+2 = 104
104

###
# Testsbksl-nlf = trinome(1, 1, 1)bksl-nlassert f(2) == 7bksl-nlassert f(0) == 1bksl-nlassert trinome(3, -1, 2)(6) == 104bksl-nlbksl-nl# Autres testsbksl-nlfor a in range(-5, 6):bksl-nl if a != 0:bksl-nl for b in range(-5, 6):bksl-nl for c in range(-5, 6):bksl-nl f = trinome(a, b, c)bksl-nl for x in range(-10, 11):bksl-nl attendu = apy-strxpy-strx + bpy-strx + cbksl-nl assert f(x) == attendu, f"On devrait avoir trinome({a}, {b}, {c})(x) == {attendu} et pas {f(x)}"bksl-nlbksl-nl 5/5
def trinome(a, b, c):bksl-nl return lambda x: ...bksl-nlbksl-nlbksl-nl# Testsbksl-nlf = trinome(1, 1, 1)bksl-nlassert f(2) == 7bksl-nlassert f(0) == 1bksl-nlassert trinome(3, -1, 2)(6) == 104bksl-nlbksl-nlbksl-nldef trinome(a, b, c):bksl-nl return lambda x: apy-strxpy-strx + bpy-strx + cbksl-nlbksl-nl



###
# Testsbksl-nlf = trinome(1, 1, 1)bksl-nlassert f(2) == 7bksl-nlassert f(0) == 1bksl-nlassert trinome(3, -1, 2)(6) == 104bksl-nlbksl-nl# Autres testsbksl-nlfor a in range(-5, 6):bksl-nl if a != 0:bksl-nl for b in range(-5, 6):bksl-nl for c in range(-5, 6):bksl-nl f = trinome(a, b, c)bksl-nl for x in range(-10, 11):bksl-nl attendu = apy-strxpy-strx + bpy-strx + cbksl-nl assert f(x) == attendu, f"On devrait avoir trinome({a}, {b}, {c})(x) == {attendu} et pas {f(x)}"bksl-nlbksl-nl 5/5
trinome = lambda a, b, c: lambda x: ...bksl-nlbksl-nl# Testsbksl-nlf = trinome(1, 1, 1)bksl-nlassert f(2) == 7bksl-nlassert f(0) == 1bksl-nlassert trinome(3, -1, 2)(6) == 104bksl-nlbksl-nltrinome = lambda a, b, c: lambda x: apy-strxpy-strx + bpy-strx + cbksl-nlbksl-nl



III. Exemples de fonctions d'ordre supérieurs map et filter en python⚓︎

La fonction map

La fonction map est une fonction qui permet d’appliquer un traitement à tous les éléments d’un itérable. Cette fonction ne modifie pas l'objet' de départ : elle renvoie un objet (itérable) encapsulant le résultat (le résultat n’est pas construit à l’appel) ; les valeurs sont calculées lorsqu’elles sont requises ; c’est une mise en œuvre du principe d’évaluation paresseuse. Le traitement est bien sûr spécifié via une fonction.

Appliquer une fonction à chaque élément d'un itérable - 1

Exécuter le code ci-dessous, observer le résultat.

###
def carre(x):bksl-nl return x py-strpy-str 2bksl-nlbksl-nlnombres = (1, 2, 3, 4, 5)bksl-nlresultat = map(carre, nombres)bksl-nlbksl-nlprint(resultat) # Un objet itérablebksl-nlprint(type(resultat))bksl-nlprint(list(resultat))bksl-nlbksl-nlbksl-nl



Appliquer une fonction à chaque élément d'un itérable - 2

On aurait pu utiliser une fonction anonyme. Exécuter le code ci-dessous, observer le résultat.

###
nombres = (1, 2, 3, 4, 5)bksl-nlresultat = map(lambda x: x py-strpy-str 2, nombres)bksl-nlbksl-nlprint(resultat) # Un objet itérablebksl-nlprint(type(resultat))bksl-nlprint(list(resultat))bksl-nlbksl-nlbksl-nl



La fonction filter

La fonction filter est un autre exemple de fonction d’ordre supérieur s’appliquant à des objets itérables. Elle prend en premier paramètre une fonction à valeur booléenne appelée filtre, et un objet itérable en deuxième paramètre. En résultat, elle renvoie un iterable ne contenant que les valeurs de la liste pour lesquels le filtre renvoie la valeur True.

Appliquer un filtre à chaque élément d'un itérable - 1

Exécuter le code ci-dessous, observer le résultat.

###
def estpy-undpair(n):bksl-nl return n % 2 == 0bksl-nlbksl-nlnombres = (1, 2, 3, 4, 5)bksl-nlresultat = filter(estpy-undpair, nombres)bksl-nlbksl-nlprint(resultat) # Un objet itérablebksl-nlprint(type(resultat))bksl-nlprint(list(resultat))bksl-nlbksl-nl



Appliquer un filtre à chaque élément d'un itérable - 2

On aurait pu utiliser une fonction anonyme. Exécuter le code ci-dessous, observer le résultat.

###
nombres = (1, 2, 3, 4, 5)bksl-nlresultat = filter(lambda x: x % 2 == 0, nombres)bksl-nlbksl-nlprint(resultat) # Un objet itérablebksl-nlprint(type(resultat))bksl-nlprint(list(resultat))bksl-nlbksl-nl



Crédits⚓︎

Frédéric Junier, Eduscol,