Aller au contenu

Les paradigmes de programmation

1) introduction⚓︎

Jusqu'à présent nous avons vu un seul paradigme de programmation (un paradigme est une manière de voir les choses, une représentation du monde) : la programmation impérative. 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)

La programmation impérative est loin d'être le seul paradigme de programmation (même si c'est sans doute le plus courant). Nous allons étudier deux autres paradigmes : le paradigme objet et le paradigme fonctionnel.

2) le paradigme fonctionnel⚓︎

Comme dit l'année dernière dans la partie du cours consacrée aux effets de bord, le paradigme fonctionnel cherche à éviter au maximum les effets de bord, dit autrement, en programmation fonctionnelle on va éviter de modifier les valeurs associées à des variables. Pour ce faire, on va chercher au maximum à utiliser les fonctions (d'où le nom de programmation fonctionnelle), mais ces fonctions ne devront pas modifier les variables : en programmation fonctionnelle, on s’efforce de coder des fonctions qui ne modifient pas l’état courant des variables. Les fonctions utilisées en programmation fonctionnelle sont parfois appelées "fonction pure" : le résultat renvoyé par une fonction pure doit uniquement dépendre des paramètres passés à la fonction et pas des valeurs externes à la fonction (elle ne doit pas non plus engendrer d'effet de bord):

Intéressons-nous au programme Python suivant :

Python
i = 5
def fct():
  if i > 5:
    return True
  else :
    return False
fct()
La fonction ci-dessus n'est pas une fonction pure, car la valeur renvoyée par la fonction fct (True ou False) dépend d'une valeur extérieure à la fonction.

Alors que dans ce cas :

Python
def fct(i):
  if i > 5:
    return True
  else :
    return False
fct(5)
La fonction ci-dessus est une fonction pure, car la valeur renvoyée par la fonction fct (True ou False) dépend uniquement du paramètre passé à la fonction.

Même si certains langages de programmation ont été conçus pour "imposer" au programmeur le paradigme fonctionnel (Lisp, Scheme, Haskell...), il est tout à fait possible d'utiliser le paradigme fonctionnel avec des langages de programmation plus "généralistes" (Python par exemple).

Nous allons maintenant travailler sur un exemple de programme Python utilisant le paradigme fonctionnel :

Considérons le programme suivant :

Python
l = [4,7,3]
def ajout(i):
  l.append(i)
Le programme ci-dessus ne respecte pas le paradigme fonctionnel, car nous avons un effet de bord (la variable l est modifiée par la fonction ajout).

Alors que dans le cas ci-dessous :

Python
def ajout(i,l):
  tab = l + [i]
  return tab
La fonction ajout ne modifie aucune variable, elle crée un nouveau tableau (tab) à partir du tableau l et du paramètre i (le signe + permet de créer un nouveau tableau, ce nouveau tableau est constitué des éléments contenus dans le tableau l auxquels on ajoute la valeur i), la fonction renvoie le tableau ainsi créé.

D'une façon plus générale, la méthode append de Python ne respecte pas le paradigme fonctionnel puisque append modifie une donnée existante. Le paradigme fonctionnel va amener le programmeur non pas à modifier une valeur existante, mais plutôt à créer une nouvelle grandeur à partir de la grandeur existante : une grandeur existante n'est jamais modifiée, donc aucun risque d'effet de bord.

3) le paradigme objet⚓︎

La programmation orientée objet repose, comme son nom l'indique, sur le concept d'objet.

Un objet dans la vie de tous les jours, vous connaissez, mais en informatique, qu'est ce que c'est ? Une variable ? Une fonction ? Ni l'un ni l'autre, c'est un nouveau concept.

Imaginez un objet (de la vie de tous les jours) très complexe (par exemple un moteur de voiture) : il est évident qu'en regardant cet objet, on est frappé par sa complexité (pour un non spécialiste). Imaginez que l'on enferme cet objet dans une caisse et que l'utilisateur de l'objet n'ait pas besoin d'en connaître son principe de fonctionnement interne pour pouvoir l'utiliser. L'utilisateur a, à sa disposition, des boutons, des manettes et des écrans de contrôle pour faire fonctionner l'objet, ce qui rend son utilisation relativement simple. La mise au point de l'objet (par des ingénieurs) a été très complexe, en revanche son utilisation est relativement simple. Programmer de manière orientée objet, c'est un peu reprendre cette idée : utiliser des objets sans se soucier de leur complexité interne. Pour utiliser ces objets, nous n'avons pas à notre disposition des boutons, des manettes ou encore des écrans de contrôle, mais des attributs et des méthodes (nous aurons l'occasion de revenir longuement sur ces 2 concepts). Un des nombreux avantages de la programmation orientée objet (POO), est qu'il existe des milliers d'objets (on parle plutôt de classes, mais là aussi nous reviendrons sur ce terme de classe est peu plus loin) prêts à être utilisés (vous en avez déjà utilisé de nombreuses fois sans le savoir). On peut réaliser des programmes extrêmement complexes uniquement en utilisant des classes préexistantes.

Les idées sous-tendant le paradigme objet datent des années 60. Mais il faudra attendre le début des années 70 et la mise au point du langage Smalltalk pour que le paradigme objet gagne en popularité chez les informaticiens. Aujourd'hui de nombreux langages permettent d'utiliser le paradigme objet : C++, Java,...

Pour nous initier à la programmation orientée objet nous allons utiliser un langage que vous connaissez bien : Python. Python permet d'utiliser le paradigme impératif (comme nous l'avons fait jusqu'à présent), mais il permet aussi d'utiliser le paradigme objet. Il est même possible, comme nous le verrons plus loin, d'utiliser les 2 paradigmes dans un même programme.

La création d'une classe en python commence toujours par le mot class. Ensuite toutes les instructions de la classe seront indentées :

Python
class LeNomDeMaClasse:
  #instructions de la classe
#La définition de la classe est terminée.

La classe est une espèce de moule (nous reviendrons plus tard sur cette analogie qui a ses limites), à partir de ce moule nous allons créer des objets (plus exactement nous parlerons d'instances). Par exemple, nous pouvons créer une classe voiture, puis créer différentes instances de cette classe (Peugeot407, Renault Espace,...). Pour créer une de ces instances, la procédure est relativement simple :

Python
peugeot407 = Voiture()

Cette ligne veut tout simplement dire : "crée un objet (une instance) de la classe Voiture que l'on nommera peugeot407."

Ensuite, rien ne nous empêche de créer une deuxième instance de la classe Voiture :

Python
renaultEspace = Voiture()

Nous rencontrons ici la limite de notre analogie avec le moule. En effet 2 objets fabriqués avec le même moule seront (définitivement) identiques, alors qu'ici nos 2 instances pourront évoluer différemment.

Pour développer toutes ces notions (et d'autres), nous allons écrire un premier programme :

Nous allons commencer par écrire une classe Personnage (qui sera dans un premier temps une coquille vide) et, à partir de cette classe créer 2 instances : bilbo et gollum :

Python
class Personnage:
  pass
gollum = Personnage()
bilbo = Personnage()
Pour l'instant, notre classe ne sert à rien et nos instances d'objet ne peuvent rien faire. Comme il n'est pas possible de créer une classe totalement vide, nous avons utilisé l'instruction pass qui ne fait rien. Ensuite nous avons créé 2 instances de la classe Personnage : gollum et bilbo.

Comme expliqué précédemment, une instance de classe possède des attributs et des méthodes. Commençons par les attributs :

Un attribut possède une valeur (un peu comme une variable). Nous allons associer un attribut vie à notre classe Personnage (chaque instance aura un attribut vie, quand la valeur de vie deviendra nulle, le personnage sera mort !)

Ces attributs s'utilisent comme des variables, l'attribut vie pour bilbo sera noté :

Python
bilbo.vie
de la même façon l'attribut vie de l'instance gollum sera noté :

Python
gollum.vie

Considérons maintenant le programme suivant :

Python
class Personnage:
  pass
gollum=Personnage()
gollum.vie=20
bilbo=Personnage()
bilbo.vie=20
Comme pour une variable il est possible d'utiliser la console Python pour afficher la valeur référencée par un attribut. Il suffit de taper dans la console gollum.vie ou bilbo.vie (sans bien sûr avoir oublié d'exécuter le programme au préalable.). Si nous tapons dans la console gollum.vie nous aurons 20 comme réponse, même chose si nous tapons bilbo.vie

Cette façon de faire n'est pas très "propre" et n'est pas une bonne pratique

En effet, nous ne respectons pas un principe de base de la POO : l'encapsulation

Il ne faut pas oublier que notre classe doit être "enfermée dans une caisse" pour que l'utilisateur puisse l'utiliser facilement sans se préoccuper de ce qui se passe à l'intérieur, or, ici, ce n'est pas vraiment le cas.

En effet, les attributs (gollum.vie et bilbo.vie), font partie de la classe et devraient donc être enfermés dans la "caisse" !

Pour résoudre ce problème, nous allons définir les attributs, dans la classe, à l'aide d'une méthode (une méthode est une fonction définie dans une classe) d'initialisation des attributs.

Cette méthode est définie dans le code source par la ligne :

Python
def __init__ (self)

La méthode init est automatiquement exécutée au moment de la création d'une instance. Le mot self est obligatoirement le premier argument d'une méthode (nous reviendrons ci-dessous sur ce mot self)

Nous retrouvons ce mot self lors de la définition des attributs. La définition des attributs sera de la forme :

Python
self.vie=20

Le mot self représente l'instance. Quand vous définissez une instance de classe (bilbo ou gollum) le nom de votre instance va remplacer le mot self.

Dans le code source, nous allons avoir :

Python
class Personnage:
  def __init__ (self):
    self.vie=20

Ensuite lors de la création de l'instance gollum, python va automatiquement remplacer self par gollum et ainsi créer un attribut gollum.vie qui aura pour valeur de départ la valeur donnée à self.vie dans la méthode init

Il se passera exactement la même chose au moment de la création de l'instance bilbo, on aura automatiquement la création de l'attribut bilbo.vie.

Si nous saisissons le programme suivant :

Python
class Personnage:
  def __init__(self):
    self.vie=20
gollum=Personnage()
bilbo=Personnage()

et que nous tapons dans la console Python gollum.vie, nous obtiendrons bien 20.

Au moment de la création de l'instance gollum, on passe comme argument le nombre de vies (gollum=Personnage (20)). Ce nombre de vies est attribué au premier argument de la méthode init , la variable nbreDeVie (nbreDeVie n'est pas tout à fait le premier argument de la méthode init puisque devant il y a self, mais bon, self étant obligatoire, nous pouvons dire que nbreDeVie est le premier argument non obligatoire).

N.B. Je parle bien de variable pour nbreDeVie (car ce n'est pas un attribut de la classe personnage puisqu'elle ne commence pas par self).

Nous pouvons passer plusieurs arguments à la méthode init (comme pour n'importe quelle fonction).

Nous allons créer 2 nouvelles méthodes :

  • Une mĂ©thode qui enlèvera un point de vie au personnage blessĂ©

  • Une mĂ©thode qui renverra le nombre de vies restantes

Intéressons-nous à ce programme :

Python
class Personnage:
  def __init__(self, nbreDeVie):
    self.vie=nbreDeVie
  def donneEtat (self):
    return self.vie
  def perdVie (self):
    self.vie=self.vie-1
gollum = Personnage(20)
bilbo = Personnage(15)

si dans la console Python vous tapez successivement :

  • gollum.donneEtat() vous allez obtenir 20

  • bilbo.donneEtat() vous allez obtenir 15

  • gollum.perdVie()

  • gollum.donneEtat() vous allez obtenir 19

  • bilbo.perdVie()

  • bilbo.donneEtat() vous allez obtenir 14

Vous avez sans doute remarqué que lors de "l'utilisation" des instances biblo et gollum, nous avons uniquement utilisé des méthodes et nous n'avons plus directement utilisé des attributs (plus de "gollum.vie"). Il est important de savoir qu'en dehors de la classe l'utilisation des attributs est une mauvaise pratique en programmation orientée objet : les attributs doivent rester "à l'intérieur" de la classe, l'utilisateur de la classe ne doit pas les utiliser directement. Il peut les manipuler, mais uniquement par l'intermédiaire d'une méthode (la méthode self.perdVie() permet de manipuler l'attribut self.vie)

Pour l'instant nous avons utilisé les méthodes uniquement en tapant des instructions dans la console, il est évidemment possible d'utiliser ces méthodes directement dans votre programme :

Python
class Personnage:
  def __init__(self, nbreDeVie):
    self.vie=nbreDeVie
  def donneEtat (self):
    return self.vie
  def perdVie (self):
    self.vie=self.vie-1

bilbo = Personnage(15)
bilbo.perdVie()
point=bilbo.donneEtat()
Après l'exécution du programme ci-dessus, la variable point aura pour valeur 14

Selon le type d'attaque subit, le personnage peut perdre plus ou moins de points de vie. Pour tenir compte de cet élément, il est possible d'ajouter un paramètre à la méthode perdVie :

Python
class Personnage:
  def __init__(self, nbreDeVie):
    self.vie=nbreDeVie
  def donneEtat (self):
    return self.vie
  def perdVie (self,nbPoint):
    self.vie=self.vie-nbPoint
bilbo = Personnage(15)
bilbo.perdVie(2)
point=bilbo.donneEtat()

Après l'exécution du programme ci-dessus, la variable point aura pour valeur 13

Il est possible d'ajouter une part d'aléatoire dans la méthode perdVie :

Python
import random
class Personnage:
  def __init__(self, nbreDeVie):
    self.vie=nbreDeVie
  def donneEtat (self):
    return self.vie
  def perdVie (self):
    if random.random()>0.5:
      nbPoint = 1
    else :
      nbPoint = 2
    self.vie=self.vie-nbPoint
bilbo = Personnage(15)
bilbo.perdVie()
point=bilbo.donneEtat()

N.B : random.random() renvoie une valeur aléatoire comprise entre 0 et 1

Comme vous l'avez remarqué, il est possible d'utiliser une instruction conditionnelle (if / else) dans une méthode. Il est donc possible d'utiliser dans le même programme le paradigme objet et le paradigme impératif.

Il est maintenant possible d'organiser un combat virtuel entre nos 2 personnages grâce à la classe Personnage que nous venons de créer :

Python
import random

class Personnage:
  def __init__(self, nbreDeVie):
    self.vie=nbreDeVie
  def donneEtat (self):
    return self.vie
  def perdVie (self):
    if random.random()>0.5:
      nbPoint = 1
    else :
      nbPoint = 2
    self.vie=self.vie-nbPoint
def game():
  bilbo = Personnage(20)
  gollum = Personnage(20)
  while bilbo.donneEtat()>0 and gollum.donneEtat()>0 :
    bilbo.perdVie()
    gollum.perdVie()
  if bilbo.donneEtat()<=0 and gollum.donneEtat()>0:
    msg = f"Gollum est vainqueur, il lui reste encore {gollum.donneEtat()} points alors que Bilbo est mort"
  elif gollum.donneEtat()<=0 and bilbo.donneEtat()>0:
    msg = f"Bilbo est vainqueur, il lui reste encore {bilbo.donneEtat()} points alors que Gollum est mort"
  else :
    msg = "Les deux combattants sont morts en mĂŞme temps"
  return msg

Nous avons encore ici la démonstration qu'il est possible d'utiliser le paradigme objet et le paradigme impératif dans un même programme.

4) conclusion⚓︎

Il est important de bien comprendre qu'un programmeur doit maitriser plusieurs paradigmes de programmation (impératif, objet ou encore fonctionnelle). En effet, il sera plus facile d'utiliser le paradigme objet dans certains cas alors que dans d'autres situations, l'utilisation du paradigme fonctionnel sera préférable. Être capable de choisir le "bon" paradigme en fonction des situations fait partie du bagage de tout bon programmeur.

Il est aussi important de bien comprendre que la frontière entre ces différents paradigmes est parfois floue, par exemple on utilise très souvent de l'impératif en programmation orientée objet.