paradigmes

Identification

Infoforall

41 - Paradigmes de programmation


Nous allons voir qu'il existe différentes façons de concevoir un langage de programmation. Vous pourrez distinguer trois manières de programmer :

  1. La programmation impérative
  2. La programmation objet
  3. La programmation fonctionnelle

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Résumé : Version HTML ou fond blanc ou ou PDF (couleur ou gris)

1 - Paradigme

Paradigme

Le mot paradigme représente la façon dont on se représente le fonctionnement du monde. Pour avoir une telle vision du monde, le paradigme s'appuie sur des fondements définis par un modèle, qu'il soit scientifique, philosophique ou même religieux.

Bien entendu, les découvertes scientifiques ont fait évoluer ou disparaître certains paradigmes purement philosophiques ou religieux. On notera par exemple l'ancien paradigme aristotélicien qui plaçait la Terre au centre de l'Univers (un géocentrisme stable et un ordre immuable des éléments) qui a disparu suite aux idées de Galilée et des travaux de Copernic notamment. Le paradigme qui l'a remplacé est basé sur une approche modélisatrice copernicienne de l'Univers, proposant d'adopter l'héliocentrisme et les mouvements satellitaires.

Paradigme de programmation

L'informatique n'explique pas comment fonctionne le monde, mais elle peut être utilisée pour modéliser le monde. Du moins, au moins, un petit problème à la fois !

Un paradigme de programmation explique donc les grands principes qu'on veut voir appliquer dans le programme. Il s'agit bien également d'une sorte de philosophie, mais une philosophie de conception.

Un paradigme de programmation est la façon de formuler et traiter les problèmes.

La plupart des langages permettent d'utiliser plusieurs paradigmes de programmation, tout en en mettant souvent un en avant.

2 - Paradigme impératif

2.1 Paradigme impératif

Idée générale

Ce paradigme est le premier que vous ayez rencontré en NSI : la résolution du problème est basée sur l'exécution progressive d'une suite d'instructions qui vont modifier l'état des variables du programme.

Grands principes

Les principes importants du paradigme impératif sont :

  1. la séquentialité (séquence d'instructions à suivre)
  2. les variations d'états des variables (on peut affecter des valeurs aux variables et faire varier cet état)
  3. les opérations de branchements (qui permettent de rompre la séquentialité pure) dont l'instuction fondamentale est le jump/goto/branch

Ce paradigme est fortement lié à l'assembleur et au langage machine en lui-même.

Exemple 1 avec de l'assembleur type ARM 32 bits (on considérera la simplification suivante : chaque instruction prend 32 bits d'espace mémoire)

0x00010000 0x00010004 0x00010008 0x0001000B 0x0001000F 0x00010013 0x00010017 0x0001002A 0x0001002E
MOV R0 #0 // on place 0 dans le registre 0 MOV R1 #5 // on place 5 dans le registre 1 CMP R0 R1 // on compare le contenu de R0 et de R1 via l'accumulateur JEQ 0x00010000 // si c'est égal, partir à l'adresse donnée ADD R0 #1 // on incrémente R0 de 1 JMP 0x00010008 // on part à cette adresse MOV R7 #1 // on stocke 1 dans le registre qui contient l'ordre à faire (1 pour exit) MOV R0 #0 // on place 0 dans le registre qui contient la réponse (0 pour tout c'est bien passé) SVC 0 // supervisor call : demande d'appel système basé sur l'ordre dans R7(exit)

Assez rapidement, on a rajouté d'autres types d'instructions de branchement, permettant de remplacer les JUMP/GOTO directs, sans nécessité de connaître l'adresse exacte en RAM de la prochaine instructions voulues !

  1. les instructions conditionnelles (if-elif-else) ;
  2. les boucles bornées et non bornées (for, while, do ... while) ;
  3. la déclaration et l'appel de fonction (qui permet d'organiser le code mais avec un appel qui provoque un retour exactement à l'endroit d'où on est parti).

Exemple 2 : programme Python équivalent au programme 1

1 2 3 4
k = 0 // on place 0 dans la variable k m = 5 // on place 5 dans la variable m while k != m: k = k + 1

Exemple 3 d'un code de combat en impératif

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
vie_a = 400 # Points de vie for_a = 60 # Force res_a = 63 # Résistance / Armure nom_a = "Alice" # Nom du personnage vie_b = 300 for_b = 70 res_b = 54 nom_b = "Bob" while vie_a > 0 and vie_b > 0: vie_a = vie_a - for_b + res_a vie_b = vie_b - for_a + res_b if vie_a > 0: gagnant = 1 elif vie_b > 0: gagnant = 2 else: gagnant = 0 if gagnant == 0: print("Aucun gagnant : les deux combattants sont ko") elif gagnant == 1: print(f"{nom_a} a gagné et il lui reste {vie_a} PV") else: print(f"{nom_b} a gagné et il lui reste {vie_b} PV")
2.2 Le fameux GOTO

Le langage machine et l'assembleur qui en est une sorte de traduction mot à mot sont basés sur ce paradigme . Ils comportent d'ailleurs des JUMP/GOTO directs permettant d'atteindre une ligne particulière sans devoir revenir au point de départ du saut comme avec une fonction.

Voici un exemple de programme utilisant un Python imaginaire intégrant ce principe de goto :

Exemple n°4

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
vie_a = 400 for_a = 60 res_a = 63 nom_a = "Alice" vie_b = 300 for_b = 70 res_b = 54 nom_b = "Bob" if vie_a <= 0 or vie_b <= 0: goto 17 vie_a = vie_a - for_b + res_a vie_b = vie_b - for_a + res_b goto 11 if vie_a > 0: gagnant = 1 elif vie_b > 0: gagnant = 2 else: gagnant = 0 if gagnant == 0: print("Aucun gagnant : les deux combattants sont ko") elif gagnant == 1: print(f"{nom_a} a gagné et il lui reste {vie_a} PV") else: print(f"{nom_b} a gagné et il lui reste {vie_b} PV")

Il s'agit donc bien qu'une boucle TANT QUE réalisée avec un branchement goto.

Pourquoi faire disparaître goto ?

Pourquoi avoir fait disparaître une instruction aussi puissante de Python plutôt que de la laisser ? Reprenons le même exemple mais dans lequel nous avons inséré quelques commentaires.

Exemple n°5

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
# Initialisation des variables définissant l'état au début vie_a = 400 for_a = 60 res_a = 63 nom_a = "Alice" vie_b = 300 for_b = 70 res_b = 54 nom_b = "Bob" # Evolution séquentielle des états des variables if vie_a <= 0 or vie_b <= 0: goto 23 vie_a = vie_a - for_b + res_a vie_b = vie_b - for_a + res_b goto 15 # Résolution du problème une fois l'état final atteint if vie_a > 0: gagnant = 1 elif vie_b > 0: gagnant = 2 else: gagnant = 0 # Affichage du résultat (ou inscription dans un fichier ou autre) if gagnant == 0: print("Aucun gagnant : les deux combattants sont ko") elif gagnant == 1: print(f"{nom_a} a gagné et il lui reste {vie_a} PV") else: print(f"{nom_b} a gagné et il lui reste {vie_b} PV")
  1. Si on insérait une nouvelle ligne, il fallait changer toutes les valeurs associées à un goto !
  2. La compréhension du programme n'est vraiment pas facilité par ces sauts ;
  3. il est difficile de prouver la correction d'un programme comportant des sauts car il faut gérer tous les cas.

Historiquement, les premières versions de FORTRAN ou le langage BASIC en contiennent.

Le langage C et C++ permettent de faire des gotos mais en version améliorée : on désigne la ligne désirée à l'aide d'un label et pas de son numéro qui peut varier.

En pseudo-code Python, cela donnerait ceci :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
# Initialisation des variables définissant l'état au début vie_a = 400 for_a = 60 res_a = 63 nom_a = "Alice" vie_b = 300 for_b = 70 res_b = 54 nom_b = "Bob" # Evolution séquentielle des états des variables DEPART: if vie_a <= 0 or vie_b <= 0: goto SORTIE vie_a = vie_a - for_b + res_a vie_b = vie_b - for_a + res_b goto DEPART # Résolution du problème une fois l'état final atteint SORTIE: if vie_a > 0: gagnant = 1 elif vie_b > 0: gagnant = 2 else: gagnant = 0 # Affichage du résultat (ou inscription dans un fichier ou autre) if gagnant == 0: print("Aucun gagnant : les deux combattants sont ko") elif gagnant == 1: print(f"{nom_a} a gagné et il lui reste {vie_a} PV") else: print(f"{nom_b} a gagné et il lui reste {vie_b} PV")
2.3 Evolutions

Il existe de nombreuses évolutions depuis le paradigme impératif de base. Vous n'avez pas à connaître leurs noms ou la classification exacte, je ne les propose ici qu'au titre de l'histoire de l'informatique :

  • Sans goto : on les remplace par les boucles et les instructions conditionnelles de façon à obtenir une programmation plus structurée.
  • Avec des routines : on encapsule les tâches dans des routines - procédures (routines sans retour) - fonctions (routines avec retour) ne dépassant pas quelques dizaones de lignes.
  • Avec des modules : on encapsule encore plus les tâches en le plaçant cette fois dans des fichiers séparés.

Ces évolutions ne sont pas propres à ce paradigme et on retrouvera des choses similaires dans les deux autres paradigmes que nous allons rencontrer aujourd'hui.

01° Etudier le code suivant puis répondre aux questions :

  1. ta est-elle une variable globale ou locale ?
  2. tb est-elle une variable globale ou locale ?
  3. tc est-elle une variable globale ou locale ?
  4. En Python, quelle va être la différence entre les lignes 4 et 5 ?
  5. quel va être le contenu du tableau ta après l'appel de la fonction ?
1 2 3 4 5 6 7 8 9 10 11
def mystere(): """Agit (ou pas ?) sur le tableau-paramètre p reçu""" tb = [v for v in ta] tc = ta tb[0] = 100 tc[1] = 200 ta = [10, 20, 30] mystere()

...CORRECTION...

  1. ta est-elle une variable globale ou locale ? Globale, son espace des noms est le programme principal (le "main")
  2. tb est-elle une variable globale ou locale ? Locale, elle n'appartient qu'à l'espace des noms de la fonction
  3. tc est-elle une variable globale ou locale ? Locale, elle n'appartient qu'à l'espace des noms de la fonction
  4. En Python, quel va être la différence entre les lignes 4 et 5 ?
    1. Ligne 4 : on crée une copie du tableau - on obtient deux espaces-mémoires qui vont contenir la même chose.
    2. Ligne 5 : on crée juste un alias - on obtient deux variables qui pointent vers la même zone mémoire.
  5. quel va être le contenu du tableau ta après l'appel de la fonction ? Puisque tc réference la même zone mémoire que ta, lorsque modifie la case numéro 1 en ligne 7, on modifie directement le tableau auquel ta fait référence. Du coup, la variable ta fait référence à [10, 200, 30] après le passage de cette ligne.
principe des alias

02° Comment se nomme l'effet qui permet de modifier le tableau ta ?

Expliquer le phénomène en utilisant les mots variables, alias et zone mémoire.

...CORRECTION...

Il s'agit de l'effet de bord.

Ce phénomène apparaît lorsque deux variables sont simplement des alias vers la même zone-mémoire : si on modifie la zone mémoire, on "modifie" nécessairement les deux variables puisque les deux référencent les mêmes cases.

On peut donc modifier une même zone mémoire en utilisant un alias de cette zone. Ca peut être pratique. Ou pas.

03° Que devrait afficher ce programme ?

Attention : en Python, l'évaluation du range de la boucle ne se fait qu'au départ.

1 2 3 4 5 6 7
a = ["A", "B", "C"] b = a c = b for i in range(len(a)): c.append(a[i]) print(a)

...CORRECTION...

Les variables a, b et c sont trois alias vers une même zone mémoire.

Agir sur c revient donc à agir sur a.

Rajouter des éléments à c revient donc à rajouter des éléments à a... : elles désignent le même tableau.

C'est comme si on avait écrit ceci :

1 2 3 4 5
a = ["A", "B", "C"] for i in range(3): a.append(a[i]) print(a)
['A', 'B', 'C', 'A'] ['A', 'B', 'C', 'A', 'B'] ['A', 'B', 'C', 'A', 'B', 'C']
2.4 Effets de bord

Les effets de bord sont donc les modifications d'une même zone mémoire référencée par plusieurs variables, qu'on nommera des alias.

Avantage : comme on ne transfère que l'adresse ou la référence, il n'est pas plus compliqué de "transférer" un tableau de 1 millions d'éléments à une fonction qu'un tableau de 10 éléments.

Désavantage : on peut finir par modifier quelque chose qu'on ne voulait pas vraiment modifier !

Conséquence : il faut indiquer dans la documentation de vos fonctions lorsqu'il y a modification d'un paramètre par effet de bord.

C'est pour cela que nous avions vu en 1er deux façons de créer des fonctions gérant les tableaux :

2.5 Modifier un tableau par effet de bord depuis une fonction

Une seule façon de faire : l'accès par indice.

1 2 3 4 5 6
def modifier(t:list) -> None: for i in range( len(t) ): t[i] = t[i] ** 2 notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] modifier(notes)
2.6 Renvoyer un nouveau tableau basé sur un tableau-paramètre depuis une fonction

Deux façons : toujours l'index ou alors on modifie directement le tableau par compréhension.

  • Créer une copie du tableau initial
  • Modifier la copie en utilisant les index
  • Renvoyer la copie (et la stocker dans une variable !)
1 2 3 4 5 6 7 8
def copier_et_modifier_la_copie(t:list) -> list: copie = [valeur for valeur in t] for i in range( len(copie) ): copie[i] = copie[i] * 2 return copie notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] nouvelles_notes = copier_et_modifier_la_copie(notes)
1 2 3 4 5
def copier_et_modifier_la_copie(t:list) -> list: return [valeur * 2 for valeur in t] notes = [15, 18, 8, 10, 12, 15, 20, 5, 12, 17, 12, 10, 18, 4] nouvelles_notes = copier_et_modifier_la_copie(notes)

Dernier exemple qui vous permettra de voir qu(il existe parfois des effets de bord non désirés.

05° Mettre le programme en mémoire puis utiliser les instructions consoles fournies. Que va afficher la dernière commande ?

1 2 3 4 5 6 7
entrees = ['tarte au saumon', 'crudités'] plats = ['steak frites', 'ratatouille'] menus = [entrees, plats] entrees2 = entrees plats2 = ['Pates aux épinards', 'Gratin de brocolis'] menus2 = [entrees2, plats2]
>>> menus [['tarte au saumon', 'crudités'], ['steak frites', 'ratatouille']] >>> menus2 [['tarte au saumon', 'crudités'], ['Pates aux épinards', 'Gratin de brocolis']] >>> menus2[0].append('Oeufs mayo') >>> menus2 [['tarte au saumon', 'crudités', 'Oeufs mayo'], ['Pates aux épinards', 'Gratin de brocolis']] >>> menus ???

...CORRECTION...

On a modifié le contenu de menus en modifiant le contenu de menu2 !

>>> menus [['tarte au saumon', 'crudités', 'Oeufs mayo'], ['steak frites', 'ratatouille']]

On a ici le cas d'un tableau de tableaux. Nos menus ne contiennent en réalité par les tableaux mais juste les références-mémoire vers les vrais tableaux.

explications effets de bord non désirés

Conclusion : il faut faire très attention aux copies lorsqu'on utilise un langage qui utilise un paradigme impératif : beaucoup d'objets sont des objets mutables et en faire des "copies" avec une simple affectation ne permet que de recopier la référence-mémoire !

2.7 Copie d'objets mutables avec Python

1 - Copie superficielle

On crée bien deux strctures différentes mais les éléments référencent les mêmes zones mémoires.

Si les éléments sont immuables, pas de problèmes. Par contre, si les éléments sont mutables, on peut modifier le "contenu" de l'autre variable en modifiant le contenu de la première !

>>> a = [10, [20], 30] >>> b = [e for e in a] >>> b[0] = 100 >>> b[1][0] = 200 >>> b [100, [200], 30] >>> a [10, [200], 30]

Il existe aussi une méthode copy() qui permet de faire pareil que la création par compréhension. Autant utiliser la création par compréhension car elle est au programme.

>>> a = [10, [20], 30] >>> b = a.copy() >>> b[0] = 100 >>> b[1][0] = 200 >>> b [100, [200], 30] >>> a [10, [200], 30]

2 - Copie profonde

Cette fois, on copie récursivement tous les contenus en tant que nouveau contenu.

Le plus simple est d'utiliser une méthode Python qui se nomme deepcopy (on peut aussi le coder directement à la main bien entendu).

>>> import copy >>> a = [10, [20], 30] >>> b = copy.deepcopy(a) >>> b [10, [20], 30] >>> b[0] = 100 >>> b[1][0] = 200 >>> b [100, [200], 30] >>> a [10, [20], 30]

En conclusion : la programmation itérative est simple à prendre en main, mais il faut faire attention aux effets de bords indésirables.

3 - Programmation Orientée Objet (POO)

La programmation orientée objet ou POO a été créée notamment pour contrôler un peu plus ces effets de bords problématiques.

La POO est basée sur les effets de bord. Le but n'est pas de les éviter mais de bien les gérer.

3.1 Paradigme objet

Idée générale

On ne crée pas un programme principal et des fonctions effectuant les tâches qu'on doit effectuer : on gère juste les interactions entre des objets qui possèdent :

  • leurs variables : les attributs
  • leurs fonctions : les méthodes

Les grands principes : encapsulation et notion de public/privée

Pour garantir le bon fonctionnement de l'objet mutable, on travaille sur l'encapsulation des objets et en considérant les données comme privées :

  • seul un objet d'une classe B a le droit de modifier directement les attributs d'un objet d'une classe B. Les attributs sont privés ;
  • les autres objets doivent utiliser les méthodes d'interface publiques pour modifier les attributs ;
  • ces méthodes d'interface vérifient la validité des modifications avant d'appliquer les modifications : l'effet de bord existe toujours mais on surveille de près les demandes venant de l'extérieur.
  • On peut même aller plus loin si on le désire, et dire que seul l'objet lui-même peut modifier ses propres données.
interaction entre deux objets

Exemple Objet

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
class Personnage: """Classe définissant le personnage""" def __init__(self, nom:str, pv:int, fo:int, re:int): """Méthode initialisateur ou constructeur""" self.nom = nom self.vie = pv self.force = fo self.resistance = re def subir_degats(self, dgt:int): """(privée) Modifie les pv du personnage""" if type(dgt) == int and dgt > 0: self.vie = self.vie - dgt if self.vie < 0: self.vie = 0 def combattre(self, adversaire): """(privée) Self subit l'assaut de l'adversaire""" degats = adversaire.obtenir_force() - self.resistance self.subir_degats(degats) def obtenir_nom(self) -> str: """Renvoie le nom du personnage""" return self.nom def obtenir_vie(self) -> int: """Renvoie le nombre de vie restante""" return self.vie def obtenir_force(self) -> int: """Renvoie la force du personnage""" return self.force def obtenir_resistance(self) -> int: """Renvoie la résistance du personnage""" return self.resistance def lancer_combat_avec(self, adversaire) -> str: """Lance un combat entre 2 personnages et renvoie le gagnant""" while self.obtenir_vie() > 0 and adversaire.obtenir_vie() > 0: self.combattre(adversaire) adversaire.combattre(self) if self.obtenir_vie() > 0: return self.nom elif adversaire.obtenir_vie() > 0: return adversaire.nom else: return "Aucun gagnant" if __name__ == '__main__': a = Personnage("Alice", 400, 60, 63) b = Personnage("Bob", 300, 70, 54) a.lancer_combat_avec(b)

Vous remarquerez que l'un des problèmes de la POO pour les petits programmes est son côté verbeux : même en Python et sans vraiment respecter toutes les règles du paradigme, on a un code très long par rapport au même programme en impératif.

Langages adaptés

Une grande majorité des langages modernes permettent de faire de la POO mais avec une souplesse variable.

Certains imposent l'objet en mode privé-public dès les premières lignes (Java pour exemple, où même le "programme principal" est un objet)

D'autres permettent de faire de l'objet en mode privé-public si on veut (C++ par exemple)

D'autres permettent de travailler avec des objets mais sans permettre le mode privé-public. Python est l'un d'entre eux.

Enfin, certains langages ne permettent pas de faire de la programmation objet. Le C par exemple.

06° Associer le bon vocabulaire aux noms suivants :

  1. Personnage
  2. a
  3. __init__
  4. obtenir_vie
  5. self
  6. force

...CORRECTION...

  1. Personnage -> Classe
  2. a -> objet ou instance
  3. __init__ -> méthode constructeur ou méthode initialisateur
  4. obtenir_vie -> méthode
  5. self -> objet en cours d'utilisation
  6. force -> attribut

07° Expliquer ce que recoivent les deux paramètres self et adversaire de la méthode lacer_combat_avec lorsqu'on lance ceci :

>>> a.lancer_combat_avec(b) 'Alice'

08° Expliquer ensuite rapidement le comportement de cette méthode.

3.2 Attendus du BAC

Très peu de choses en réalité.

Vous devez :

  • Etre capable de reconnaître une Classe : le code qui permet de créer les objets
  • Etre capable de reconnaître un objet : un objet est une instance d'une Classe, une création concrète basée sur la Classe
  • Etre capable de reconnaître un attribut : il s'agit d'une variable stockée dans un objet (variable d'instance) ou dans la classe directement (variable de classe)
  • Etre capable de reconnaître une méthode : il s'agit d'une fonction stockée dans un objet. Contrairement à une fonction classique, elle a besoin d'au moins un premier paramètre qu'on nomme souvent self : l'instance de l'objet sur lequel elle doit travailler.
  • Accéder à une méthode : savoir faire appel à une méthode : reference.methode(...), le paramètre self recevant automatiquement reference.
  • Accéder à un attribut : lire ou modifier un attribut : reference.attribut ou reference.attribut = ...
  • Savoir définir seul une Classe et donc :
    1. Connaître la syntaxe de déclaration de la Classe (majuscule pour la première lettre du nom)
    2. Savoir déclarer la méthode constructeur
    3. Savoir déclarer des méthodes
  • Comprendre les principes généraux et l'intérêt de l'encapsulation mais sans maitrise réelle de ces principes : aucune question sur privée, publique, accesseur, mutateur par exemple. En Python, vous pouvez créer des programmes qui modifient ou lisent les attributs d'un objet sans devoir passer par des méthodes publiques.

4 - Programmation fonctionnelle

Nous avons donc vu :

  1. Le paradigme impératif qui permet des sauts dans l'exécution et des effets de bord non surveillés, ce qui rend les programmes difficiles à vérifier.
  2. Le paradigme objet qui se base sur des règles d'utilisation encadrées des effets de bords liés aux objets mutables. Le système peut être stable mais les démonstrations formelles de cette stabilité ne sont pas faciles à réaliser non plus...

Moralité : on voudrait un paradigme qui permette "facilement" de prouver la correction de notre programme. Or, ce sont les effets de bord qui posent problème.

4.1 Paradigme fonctionnel

Une fonction pure est une fonction qui calcule sa réponse uniquement à partir des paramètres qu'elle a reçu, comme une fonction mathématique. A chaque fois qu'on l'appelle en lui fournissant les mêmes arguments, elle fournit le même résultat.

Grands principes du paradigme fonctionnel

Les principes importants du paradigme fonctionnel sont :

  1. Un refus d'utiliser les effets de bord ;
  2. Il existe des fonctions d'ordre supérieur : des fonctions capables de recevoir des fonctions en tant que paramètes ou des fonctions qui renvoient l'adresse d'une fonction.
  3. Les variables ne sont pas utilisées : on utilise uniquement des constantes.
  4. Les fonctions sont toutes des fonctions pures :
  5. Les structures de données sont immuables : si on veut modifier un tableau, il faut créer un nouveau tableau.
  6. du récursif (programmation récursive) plutôt que des boucles (programmation itérative) puisqu'on ne veut pas utiliser de variables de boucles, qui par définition doivent varier.

Exemple Fonctionnel

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# Déclaration des fonctions def nv_pv(df:tuple, at:tuple) -> int: """Calcule le pv du défenseur df après attaque de l'attaquant at""" return df[0] - max(0, at[1] - df[2]) def maj(df:tuple, at:tuple) -> tuple: """Renvoie les nouvelle caractéristiques mise à jour du defenseur df après attaque de at""" return (nv_pv(df, at), df[1], df[2], df[3]) def est_hors_jeu(p): """Prédicat qui renvoie True si le personnage p à 0 pv ou moins""" return p[0] <= 0 def lancer_combat(a:tuple,b:tuple) -> str: """Lance le combat entre a et b et renvoie le nom du gagnant ou "Aucun gagnant"""" if est_hors_jeu(a) or est_hors_jeu(b): # Condition d'arret et cas de base if not est_hors_jeu(a): # a a gagné car il lui reste des pv return a[3] elif not est_hors_jeu(b): # b a gagné car il lui reste des pv return b[3] return "Aucun gagnant" else: # Cas recursif return lancer_combat(maj(a,b), maj(b,a)) # ETAT DU SYSTEME # Chaque personne est un tuple (vie-0, force-1, resistance-2, nom-3) a = (400, 60, 63, "Alice") b = (300, 70, 54, "Bob")

Programmation fonctionnelle et récursivité

Les méthodes de preuve de correction s'appliquent bien à la récursivité.

De plus, la récursivité permet de se passer des boucles et des variables variables.

La récursivité est donc centrale dans ce paradigme.

Création par compréhension en Python

Puisqu'on tente de limiter fortement les variables en fonctionnel, renvoyer un tuple, une liste ou un dictionnaire via une déclaration en compréhension est courant.

Sans boucle FOR, difficile de lire un tableau case par case ? Non, les langages fonctionnels possèdent des fonctions natives dont c'est le travail (map, filter, reduce en Python, mais elles ne sont pas au programme) : map() permet ainsi d'appliquer une fonction fournie en paramètre sur une structure itérable fournie en paramètre. Voici par exemple comment obtenir une copie d'un tableau dont tous les éléments ont été multipliés par deux par rapport au tableau initial :

1 2 3 4 5 6 7 8 9
t = [5, 10, 20] def fois2(elt:int): return 2*elt t2 = list(map(fois2, t)) print(t) print(t2)

Plutôt que d'apprendre à utiliser les fonctions qui permettent de se passer des boucles for lorsqu'on travaille sur les tableaux, nous allons faire une petite entorse au paradigme fonctionnel : nous allons autoriser la boucle for mais uniquement dans les créations par compréhénsion :

1 2 3 4 5 6
t = [5, 10, 20] t2 = [v*2 for v in t] print(t) print(t2)

09° Voici une fonction plus2(). S'agit-il d'une version impérative ou fonctionnelle ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def plus2(t:list) -> None: """Modifie t par effet de bord en rajoutant 2 à chaque valeur :: param t(list) :: le tableau qu'on veut modifier :: return(None) :: fonction-procédure :: exemple :: >>> test = [1, 2, 3] >>> plus2_iter(test) >>> test [3, 4, 5] """ for i in range(len(t)): t[i] = t[i] + 2

...CORRECTION...

Il s'agit clairement d'une version itérative : on modifie le tableau par effet de bord.

10° Voici une fonction plus2(). S'agit-il d'une version impérative ou fonctionnelle ?

1 2 3 4 5 6 7 8 9 10 11 12
def plus2_fonc(t:list) -> list: """Renvoie une copie du tableau où toutes les valeurs sont augmentées de 2 :: param t(list) :: le tableau qu'on veut modifier :: return(list) :: la nouvelle version du tableau :: exemple :: >>> plus2_fonc([1, 2, 3]) [3, 4, 5] """ return [v+2 for v in t]

...CORRECTION...

Il s'agit d'une version fonctionnelle : on crée une copie et on renvoie le résultat.

Autre exemple : comment rajouter un élément dans un tableau en Python ? Nous allons regarder d'un peu plus près comment fonctionne Python.

11° Voici trois codes permettant de rajouter un élément dans un tableau Python. Quelle version pourrait correspondre le plus à une version fonctionnelle ? Quelle est néanmoins son gros défaut du point de vue fonctionnel ?

>>> a = [4, 8] >>> id(a) 140030316223048 >>> a.append(12) >>> a [4, 8, 12] >>> id(a) 140030316223048
>>> a = [4, 8] >>> id(a) 140030316223304 >>> a = a + [12] >>> a [4, 8, 12] >>> id(a) 140030316783822
>>> a = [4, 8] >>> id(a) 140030316223368 >>> a += [12] >>> a [4, 8, 12] >>> id(a) 140030316223368

...CORRECTION...

La seule version "correcte" pour faire du fonctionnel est la version a = a + [12] : c'est la seule qui modifie bien la référence de la variable : on crée donc un nouveau tableau qui écrase l'ancien.

Mais attention, même cette version ne convient pas : a reste une variable dont le contenu à varier.

12° Créer la fonction ajouter_elt(t, elt) qui renvoie un nouveau tableau semblable à celui reçu mais en lui rajoutant un nouvel élément.

>>> ajouter_elt([1,2,3], 12) [1, 2, 3, 12]

...CORRECTION...

1 2
def ajouter_elt(t:list, elt) -> list: return t + [elt]
Attention, tout cela est artificiel : les tableaux de Python de type list sont mutables. Donc, on doit faire attention. Avec un langage purement fonctionnel, les structures de données seraient immuables.

Les tuples de Python permettent beaucoup plus facilement de gérer ce type de demandes.

13° La fonction somme ci-dessous peut-elle être considérée comme fonctionnelle ?

1 2 3 4 5
def somme(t:list) -> int: if len(t) == 0: return 0 else: return t[0] + somme([t[i] for i in range(len(t)) if i > 0])
>>> somme([1,2,3,4]) 10

...CORRECTION...

Elle respecte bien le paradigme fonctionnelle :

  1. Pas d'effet de bord
  2. La fonction ne travaille qu'avec ses paramètres
  3. La fonction renvoie constamment le même résultat pour le même appel
  4. (par de boucle for sauf pour utiliser une création par compréhension)
Langage fonctionnel

Un langage fonctionnel est un langage de programmation dont la syntaxe et les caractéristiques encouragent la programmation fonctionnelle.

Le premier fût LISP en 1958, ce n'est donc pas neuf...

Logo LISP

LISP n'est pas fortement typé.

Les langages fonctionnels récents, comme Haskell (1990), OCaml (1996) ou Scala (2004) sont fortement typés.

Le Logo de OCaml :

Logo OCaml

Le principe du codage-décodage de RSA est de calculer une mise à la puissance du message :

 mc = (me) % n 

  • n se nomme le module de chiffrement car il sert à faire un modulo et
  • e est l'exposant de chiffrement car on l'utilise en tant que mise à la puissance du message.

 md = (mcd) % n 

  • n se nomme le module de chiffrement car il sert à faire un modulo et
  • d est l'exposant de déchiffrement car on l'utilise en tant que mise à la puissance du message.

14° Demander à Python de calculer cela (message 15 et exposant 437) :

>>> 15**437

Question : est-ce rapide ?

...CORRECTION...

Oui, c'est assez rapide. Le résultat apparaît tout de suite à l'échelle humaine.

>>> 15**437 8951178286285835053504334637121363496635561744942322831981649499685880630495193742162622726037172054932552041017789554974108750974231796350083483386737588631707451372572071839923611884846146982217961948970974188343920670569488604409502932712044073128843584147870956814158980915052673498934345180446817441904416161488553516020455611352563428830901482795268646403744376800464835118010898211170598580175760537342795125970200615956585889696932345818328232800457021236281194924953863445438173584989272058010101318359375

15° Faire la même chose avec un exposant plus conséquent :

>>> 15**135478415797

Question : est-ce rapide ?

...CORRECTION...

Cette fois, ça ne répond pas immédiatement. En tous cas, pas assez rapidement pour être utilisable !

Comment faire alors ?

Comme d'habitude en informatique, nous allons "tricher". C'est à dire en réalité que nous allons refléchir à ce qu'on désire faire.

La méthode que nous allons utiliser se nomme l'exponentiation rapide : nous allons chercher à calculer des mises au carré successives.

En voici le principe :

Calcul normal

Calcul normal de 15140 = 15*15*15*15*...15 : on a donc 139 étapes et multiplications à faire.

Calcul avec exponentiation rapide

Cette fois, nous nous rendons compte de ceci :

  1. 15140 = (152) 70
  2. Or x70 = (x2) 35
  3. Notre calcul devient ( (152)2 ) 35 ou encore ( 154 ) 35

  4. Or x35 = x * x34 = x * (x2) 17
  5. Notre calcul devient 154 * ( (154)2 ) 17 ou plus simplement 154 * ( 158 )17

  6. Or x17 = x * x16 = x * (x2)8
  7. Notre calcul devient 154 * 158 * ((158)2)8 ou plus simplement 154 * 158 * ( 1516 )8

  8. Or x8 = (x2)4
  9. Notre calcul devient 154 * 158 * ( 1532 )4

  10. Or x4 = (x2)2
  11. Notre calcul devient 154 * 158 * ( 1564 )2

  12. Or x2 = (x2)1
  13. Notre calcul devient 154 * 158 * 15128

16° Calculer 2**140 et 154 * 158 * 15128

17° Créer la fonction exp_rapide qui se base sur ce principe :

Pour cela, répondre à ces questions :

  1. Quelle est la condition d'arrêt et le cas de base ?
  2. Quelles sont les cas récursifs ?
  3. Ecrire votre fonction
1 2 3 4
def exp_rapide(x:int, n:int) -> int: """Permet de calcul rapidement x**n par exponentiation rapide""" pass

...CORRECTION...

1 2 3 4 5 6 7 8 9 10
def exp_rapide(x:int, n:int) -> int: """Permet de calcul rapidement x**n par exponentiation rapide""" if n == 1: return x else: if n % 2 == 0: return exp_rapide(x**2, n//2) else: return x * exp_rapide(x**2, n//2)

Nous pouvons maintenant réaliser de gros calculs comme 151000000 :

>>> exp_rapide(15, 1000000) 18157484464073660739361385102568524158220149848872846937473094084338562416435600258825258927114364712839829341411768452311029550801259916524900246903746519194985897747020245006432814081271944944964627390236954666710145306582011601145129555022325186721853618791672595463912004171293055341938033147384489585694289498265413669724284420949477001977110845859897875368169668416114495714422296461947959977507209937715186439064438131425513901959206795441747019104649609148833199725630876047378156331387001037180808890080716116968436588642510451555643891607257450337087994552240950881199410338261320946885933869043945205267399309758938761768181132819380600243748636479585529858305543412745779370119852657135462147041964832108657853166254330303043380167232806631380820413502768186120887225393677543981648418040350193501156397505591300574834511331696240298331934319251085780560315662176327891203483732254379720710255224027465322890549801666002622259344523039609988586765296270991956661834468803909345745301430799047608302495500558343690693948328129220745764023200277377029952984985103119029840354180928246004868169711967202323511189585031193872604307556558217276333010894347418411896888231717267155445117761313373888921441963525468019987159926340524956786999153787062512693330350238088080020442921724125824990990229224986026621047252275975796495163519032619559938479056192047610795930177066733589611777217692795304485460826580506209644043580875770071488864263212193779100187334028275117826502402970704098423975010571720339143334334270354077192313602371289581218972338447556778213779871466131774583840184268158567427234759570773775512030368686898417212085011291950005236709762568732864429440242131873004926936175433405160491559414830920819506814015893331146200559984442535407970399724436186166298591741472973275358939371486155134349524930098668859475146752929925648450413541296078799713864836196993020452251256159451417237475537449678684230351079512728513948361730340263032012709380021463768853127904493201678946617415613301611947372331896819052751740059501945940243625142353038213297749537740357487832738022463710092570280527410934835331778633526504663658705851066010319534055566679004116652879900944792596178459725873371807341324460560617987425055343539911604750993584473599394401011866262759146777035239516462227960402858604975320764720716676249811016985030867932433212554349936308019154541751222688947510305661407115432428235937815751291184204742297561486400158217472677240328365012721375728341686040559157538021540694187602994952262915557008339051743531205093439095111383845107351416146624706704565702129010242736992031289090583494405881279968513327209404209445987996974868042822235518898477744954677284576728292201624680063243938566756365600869200226212446810248920998271362456852685691823629445510974521633149110954799528137420559306705001813213955078404225387223212742905011804210381146220600256257393432975638679749993723630703950576284858811308993516708791876685261339909967085932227660533552604498120253003055885339296846300778290146591152091041378211836422383650982211646689501084827708389342762046175720484056416769374081620261966749283279708494178503043203104030562495651951358994738602535270672388088087398056139573831460647692907597611719301762430315102604589125089599793406968711468579649964826977919197812689289163504785509444164647159732111762259176103709454433719399810579529191171460710232139689129082378016486801216024009455716567545624298977603153401377551381829741350420623905000223591416771426672830688582952705846295598528323075282403746742106494739391369925221320689574228543107814735088460118421109826985617399470312025828395270460607883685119278299565811950651391464348942828263150833319960179601120300686665284559784335414028205070789322705694223097616478301105679322618672621630720227522272586614206564806188640246031923348639502513883270807116452522264958246287043361083359434418923496966597679618052453979679423732729101363979491102170464898886147424733615995686043665077650657369320996388895767554028097233660522073188833606664857071349735416942282698584836586589148778434382287382258907449201404300079969441924920864351236294398025739411174915089549274809898794335761605402416204765780801371501641420824609952871660997728499855310317646623258126920301425908419399674463396476249351551505562622452673517867163217684020310411542241380411389994030667787977709262571546455573082380931380000584751817797867893195935143427768434197910864089183748850952211209977174639851824207981266862794238539283093863122873271758184675611527085359108742415139230086274251893626645177424348456529223952489891900807105037337878836587844496972978139020991947036551652298475414528419520284081274067614321933876120132648056122818174942501535094828438505717889474274136465452632529835963767298261988466315228990270463805309322257644754176835748685462665251000480667177404982131526704202024134245117530660892641896815217648770012165375085175994728429281605869622756138898611443068554669984664696867161986…

RSA ne demande pas de faire ce calcul en intégralité : on veut le modulo.

Et là, c'est intéressant : nous allons faire toutes les étapes mais en réalisant le modulo n à chaque étape, les nombres à traiter seront bien plus courts.

Nous allons donc avoir un paramètre supplémentaire : le module de chiffrement.

18° Créer la fonction exp_rapide propre à RSA : on attend trois paramètres :

1 2 3 4
def exp_rapide_rsa(m:int, e:int, n:int) -> int: """Permet de calcul rapidement m**e %n par exponentiation rapide""" pass

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11
def exp_rapide_rsa(m:int, e:int, n:int) -> int: """Permet de calcul rapidement m**e %n par exponentiation rapide""" if e == 1: return m else: print(e) if e % 2 == 0: return (exp_rapide_rsa(m**2 % n, e//2, n)) % n else: return (m * exp_rapide_rsa(m**2 % n, e//2, n)) % n

19° Calculer la transformation de m = 190000 en utilisant les valeurs suivantes

  • n = 594127815299
  • e = 135478415797

...CORRECTION...

>>> exp_rapide_rsa(190000,135478415797,594127815299)
135478415797 67739207898 33869603949 16934801974 8467400987 4233700493 2116850246 1058425123 529212561 264606280 132303140 66151570 33075785 16537892 8268946 4134473 2067236 1033618 516809 258404 129202 64601 32300 16150 8075 4037 2018 1009 504 252 126 63 31 15 7 3
262963059740

20° Créer une fonction qui respecte le paradigme fonctionnel et qui permet d'obtenir un tableau dont le contenu est doublé par rapport à un tableau fourni en entrée.

21° Créer une fonction qui modifie le tableau en multipliant les valeurs par deux. Est-encore compatible avec le paradigme fonctionnel ? Justifiez.

22° Expliquer en quoi la fonction maximum respecte bien le paradigme fonctionnelle. Expliquer son fonctionnement.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def maximum(t): """Renvoi le maximum du tableau t""" if len(t) > 0: return trouver_maximum(t, t[0]) def trouver_maximum(t, maxi): if len(t) == 0: return maxi else: t2 = [t[i] for i in range(len(t)) if i>0] if t[0] > maxi: return trouver_maximum(t2, t[0]) else: return trouver_maximum(t2, maxi)

23° On considère le programme suivant :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
import math def maximum(t): """Renvoi le maximum du tableau t""" if len(t) > 0: return trouver_maximum(t, t[0]) def trouver_maximum(t, maxi): if len(t) == 0: return maxi else: t2 = [t[i] for i in range(len(t)) if i>0] if t[0] > maxi: return trouver_maximum(t2, t[0]) else: return trouver_maximum(t2, maxi) def maximum_fonction(f, intervalle): """Renvoie le maximum d'une fonction sur un ensemble de valeurs entières""" return maximum([f(x) for x in intervalle]) def carre(x): return x*x def racine(x): return math.sqrt(x)

Quels vont être les types de paramètres de la fonction maximum_fonction lors de cet appel ? Qu'est-ce qui devrait (au moins un peu) vous surprendre ?

>>> maximum_fonction(carre, [x for x in range(1000)])
19
def maximum_fonction(f, intervalle):
Fonctions-paramètres

On peut donc transmettre la référence d'une fonction (sans ses paramètres donc, juste la référence mémoire du code de la fonction) à une autre fonction.

Les fonctions ne sont finalement que des données comme les autres. Souvenez-vous qu'elles sont de toutes manières stockées dans la même mémoire que les données. Rien d'étrange en réalité.

Il s'agit également de l'un des fondamentaux de la programmation fonctionnelle : le passage de fonctions-paramètres et pas simplement l'appel de fonctions !

Dernière remarque (et hors programme en terme de connaissances pour le DS ou le BAC) : il est parfois pénible de transmettre une fonction à une autre fonction alors qu'on ne veut finalement que l'utiliser une seule fois. Surtout en mode console par exemple.

Il existe heureusement un type de fonction qu'on peut créer à la volée : les fonction lambda

Si cela vous intéresse, direction la FAQ ci-dessous, sinon, c'est fini !

5 - FAQ

J'ai entendu parler des lambda-fonctions. C'est quoi ?

Contrairement aux fonctions normales, elles ne portent pas de nom et on ne les déclare pas avec def mais avec ... lambda.

Si on veut créer une fonction identité  x → x  à la volée :  lambda x: x 

Si on veut créer une fonction carrée  x → x2  à la volée :  lambda x: x**2 

Si on veut créer une fonction racine carrée  x → x2  à la volée :  lambda x: math.sqrt(x) 

Du coup, on peut faire des appels à notre fonction maximum_fonction de cette façon :

>>> maximum_fonction(lambda x: x, [x for x in range(1000)]) 999 >>> maximum_fonction(lambda x: x*x, [x for x in range(1000)]) 998001 >>> maximum_fonction(lambda x: x**2 - 300*x, [x for x in range(1000)]) 698301

Programmation itérative et impérative, c'est pareil ?

Lorsque le paradigme impératif a évolué en mettant de côté le goto, remplacé par des for et des while, on a parlé de programmation itérative.

Pendant longtemps, la plupart des langages basés sur le paradigme impératif ont surtout été utilisés en programmation itérative.

On continue donc à les associer mais les langages modernes impératifs permettant également d'autres types de programmation.

Et ça marche aussi avec matplotlib par exemple ?

1 2 3 4 5 6 7 8 9
import matplotlib.pyplot as plt import math def trace(f, x_min, x_max): absc = [x/10 for x in range(x_min*10, x_max*10+1)] ordo = [f(x) for x in absc] plt.plot(absc, ordo) plt.grid() plt.show()

Et on peut alors faire ceci pour tracer des courbes à la volée :

>>> trace(lambda x: x**2, -10, 10)
fonction carrée
>>> trace(lambda x: math/log2(x), 1, 100)
fonction logarithme base 2
>>> trace(lambda x: exp(x), 1, 10)
exponentielle

Programmation récursive et fonctionnelle, c'est pareil ?

Lorsque le paradigme fonctionnelle a émergé en mettant de côté les effets de bord ET le for-while, il a bien fallu trouver un moyen de remplacer les itérations via les boucles.

Pour cela, la programmation récursive est une solution très adaptée.

Mais attention, on peut aussi faire de la programmation récursive avec d'autres paradigmes.

.

Activité publiée le 07 03 2021
Dernière modification : 08 03 2021
Auteur : ows. h.