La Programmation Orientée Objet


Un bref historique de la POO

Cest vers la fin des années 60 que fut inventé le premier langage utilisant le concept de classe: il sagit du langage Simula 67 (les langages comportent souvent lannée de leur création dans leur nom). Ce langage nest plus guère utilisé de nos jours, mais il a donné naissance quelques années plus tard au langage Smalltalk 72. Ce dernier a été inventé par un des plus grand informaticien, Alan Kay. Ce brillant chercheur nest guère connu du grand public, mais il est linventeur du terme programmation orientée objet, ainsi que du concept dinterface graphique et de bureau, repris avec le succès que lon connait par Apple et Microsoft quelques années plus tard. Le langage smalltalk est toujours très utilisé dans les grandes entreprises (notamment les banques et les places financières internationnales), et sa mouture la plus moderne sintitule pharo.

Le langage smalltalk est certainement le langage orienté objet le plus pur à lheure actuelle: il nautorise quun seul paradigme de programmation, la POO (pas de programmation fonctionnelle, ni même itérative). En smalltalk, absolument tout est un objet (même une instruction conditionnelle ou bien une boucle est simulée par un objet).

Le succès des langages à objets a véritablement éclaté dans les années 1980, avec larrivée du C++ (1983), puis dans les années 1990 avec le langage java (1995) et enfin au début du nouveau siècle avec C# (2002). Ces trois langages restent parmi les langages les plus utilisés dans le monde à lheure actuelle.

Les langages dynamiques, tels python (1980), sont pour la plupart fortement orientés objets.

Les ingrédients de la POO

La POO est un paradigme de programmation qui consiste à mettre en relation des objets qui représente (en général) des concepts réls (par exemple: une voiture, un livre, une personne).

Un objet est une structure de donnée qui regroupe en son sein deux types dentités:

  • des données, appelées des attributs;
  • des fonctions constituant linterface de cet objet, appelées des méthodes;

Cest ce regroupement qui a constitué une véritable révolution dans les années 60. Il faut le mettre en comparaison avec les paradigmes alors existants, à savoir la programmation impérative et fonctionnelle. Les avantages sont multiples:

  • on regroupe dans une seule entité à la fois les données et les fonctions permettant dinteragir avec ces données;
  • ce regroupement permet notamment de cacher les détails de limplémentation de lobjet à lutilisateur, ce qui offre une grande souplesse au programmeur (notamment celle de complètement changer ces détails dimplémentations sans affecter linterface de lobjet). Ce concept très important sappelle lencapsulation.
  • La POO comporte dautres aspects (polymorphisme, héritage), mais ils ne seront pas étudiés en terminale NSI. Ils ont clairement leur utilité, mais le langage python ne fait que modérément appel à eux dans la pratique (laspect très dynamique du langage python offre dautres avantages).

Le concept de classe

La plupart des langages à objets utilisent le concept de classe. Une classe est une définition abstraite dun objet, une sorte de plan qui va servir ensuite à construire des représentations concrètes des objets.

Dautres langages utilisent plutôt le concept de prototype, comme par exemple le très célèbre javascript: pour ces derniers, les objets sont directement construits à partir dautres objets, dans une sorte de mécanisme de clônage (mais qui peut aussi comporter des mutations pour obtenir de nouvelles fonctionnalités).

En python, les classes décrit précisément la structure des objets quelle va représenter:

  • Une méthode particulière __init__, appelée le constructeur, permet notamment dinitialiser lobjet et ses différents attributs;
  • les méthodes qui serviront dinterface pour cet objet sont aussi définies au sein de la classe.
  • on peut aussi définir des méthodes qui nauront quun usage interne à lobjet. Elle ne font techniquement pas partie de linterface de celui-ci, mais seront très utiles pour son implémentation.

Une classe Rectangle

On souhaite définir une classe qui représentera un rectangle (au sens mathématique). Plusieurs représentations équivalentes sont possibles, nous choisissons de caractériser un rectangle par ses coordonnées en haut à gauche et en bas à droite.

rectangle

Notre objet comportera donc naturellement 4 attributs correspondant aux abscisses et aux ordonnées de ces deux points.

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      self.x1 = x1
      self.y1 = y1
      self.x2 = x2
      self.y2 = y2

Nous venons de définir une classe minimaliste: elle ne comporte quune unique méthode, le constructeur __init__(self, x1, x2, y1, y2). Le premier paramètre dune méthode (conventionnelement toujours appelé self, mais ce nest pas une obligation) désigne toujours lobjet qui sera défini grâce à la classe. La syntaxe self.x1 = x1 permet ici de créer un nouvel attribut x1 pour lobjet self, qui aura sa valeur initiale donnée par le paramètre x1 du même nom passé au constructeur.

Voyons comment on peut créer un objet:

>>> r = Rectangle(3, 4, 12, 9)
>>> r.x1
3
>>> r.x2
12
>>> r.x1 = 5
>>> r.x1
5

On constate que lon peut créer un objet en appelant sa classe, et en passant tous les paramètres sauf le paramètre self qui sera toujours passé automatiquement.

Examinez plus précisément ce mécanisme à laide de python tutor. Rien ne vaut une visualisation dynamique de lexécution (cliquez sur le lien précédent !), mais voici quelques explications:

  • Lors de la création de lobjet, une nouvelle instance (cest-à-dire une réalisation physique de la classe) est crée en mémoire, et est référencée par la variable self dans lenvironnement dexécution du constructeur __init__. Remarquez que les coordonnées du rectangle sont bien passées en paramètre, mais les attributs ne sont pas encore créés. Création d'un rectangle 1
  • À la fin du constructeur, les quatre attributs ont été créés. Ils portent le même nom que les paramètres du constructeur (ce nétait dailleurs pas une obligation, mais cela ne pose aucune difficulté), mais le fait quils soient préfixés par self. enlève toute ambiguïté. Création d'un rectangle 2
  • Une fois le rectangle créé, les attributs peuvent être manipulés grâce à la syntaxe utilisée depuis bien longtemps en python. Création d'un rectangle 3 À présent on comprend pourquoi dans certains cas des fonctions étaient précédées dune valeur, comme par exemple pour ma_liste.append(42): il sagissait en fait de méthodes attachées à un objet particulier (en loccurence ici un objet de classe list).

La classe que nous venons de créer est extrêmement minimaliste: on se contente de créer des attributs dans le constructeur, et cest tout. Notons que nous disposons néanmoins dune nouvelle fonctionnalité intéressante: un objet rectangle, avec quatre attributs portant un nom (que lon espère) intelligible et identifiable. Cest déjà bien plus lisible que la syntaxe rectangle[0], rectangle[1], rectangle[2], rectangle[3] que nous aurions dû utiliser si nous avions opté pour une liste ou bien un tuple.

Voyons à présent comment étoffer linterface de notre objet. Pour cela, nous allons devoir lui ajouter de nouvelles méthodes !

Ajout de méthodes à la classe Rectangle

Quelle genre dinterface pourrions-nous souhaiter pour un rectangle ? On peut notamment vouloir connaître sa longueur, sa hauteur, son périmètre, son aire. Rien de plus simple !

class Rectangle:
   ...

   def longueur(self):
      return self.x2 - self.x1

   def hauteur(self):
      return self.y2 - self.y1

   def perimètre(self):
      return 2*(self.hauteur() + self.longueur())

   def aire(self):
      return self.hauteur()*self.longueur()

Nous navons pas reproduit le constructeur ici, puisquil na pas été modifié.

Nous disposons à présent de quatre nouvelles méthodes, qui permettent daccéder à certaines fonctionnalités de notre rectangle.

>>> r = Rectangle(3, 4, 12, 9)
>>> r.longueur()
9
>>> r.hauteur()
5
>>> r.périmètre()
28
>>> r.aire()
45

Vous pouvez observer comment ces valeurs sont calculées avec python tutor.

Les quatre méthodes implémentées jusquà présent se contentaient daccéder aux attributs pour en déduire de nouvelles caractéristiques du rectangle.

Notez la présence du paramètre self, toujours situé en première position, et qui est toujours rajouté automatiquement par python lors dun appel. Concrètement, lappel r.longueur() est automatiquement traduit par python en Rectangle.longueur(r), et on comprend ainsi que la valeur que pointera self ne sera autre que lobjet r, cest-à-dire lobjet qui se trouve juste avant le point dans la syntaxe r.longueur().

Mais on peut tout aussi bien modifier les valeurs dun ou plusieurs attributs.

Ajoutons une méthode translation(dx, dy) qui prend pour paramètre les coordonnées dun vecteur, et déplace le rectangle suivant la translation définie par ce vecteur:

class Rectangle:
   ...

   def translation(self, dx, dy):
      self.x1 = self.x1 + dx
      self.x2 = self.x2 + dx
      self.y1 = self.y1 + dy
      self.y2 = self.y2 + dy

On se contente ici dajouter les coordonnées du vecteur aux deux coordonnées définissant notre rectangle.

>>> r.translation(-5, 3)
>>> r.x1, r.y1
(-2, 7)
>>> r.x2, r.y2
(7, 12)

Une meilleure encapsulation

Le rectangle ainsi défini à la section précédente est déjà bien fonctionnel, mais son utilisation par le programmeur comporte quelques dangers dont il faut avoir conscience:

  • Idéalement, on ne devrait accéder à lobjet que par lintermédiaire de son interface. Mais ici, il est tout à fait possible de modifier les attributs en dehors des méthodes de lobjet.
  • Un problème induit par le précédent: nous avons fait lhypothèse (hasardeuse) que les contraintes \(x1 \leqslant x2\) et \(y1 \leqslant y2\) seraient toujours respectées. Cest notamment sous ces hypothèse que nous avons implémenté longueur() et hauteur(). Or, nimporte qui peut changer les attributs pour leur donner des valeurs ne respectant pas ces contraintes (voire, et cest encore pire, des valeurs qui ne seraient même pas des nombres).
  • Lutilisateur de la classe Rectangle est constamment conscient de la présence de ces quatre attributs. Cela peut paraître anodin, mais cest en fait un lourd handicap: le concepteur de la classe ne pourra plus changer les attributs sans risquer de casser tous les programmes utilisant cette classe. Autrement dit: les attributs font partie de linterface de lobjet.

Illustrons ces points par un exemple:

>>> r = Rectangle(12, 4, 3, 9):
>>> r.longueur()
-9
>>> r.hauteur()
5
>>> r.périmètre()
-8
>>> r.aire()
-45

Comment pouvons-nous corriger tous ou partie de ces défauts ? Une technique très prisée par les programmeurs utilisant le paradigme objet est de sastreindre à cacher les attributs. Dailleurs, la plupart des langages objets imposent ce genre de restriction par défaut (cest le cas de C++, Java et C# notamment). En python, langage dynamique et très permissif par essence, il est possible de cacher des attributs en suivant certaines conventions.

Il y a essentiellement deux techniques utilisables:

  • On peut ajouter un simple caractère underscore _ devant le nom de lattribut. Cela nempêche pas qui que ce soit daccéder à cet attribut, mais cette convention est universellement respectée par les programmeurs python qui savent ainsi que cet attribut nest pas destiné à être accédé à la main, et quil pourra être modifié voire disparaître à tout moment.
  • Python propose un mécanisme un peu plus sûr, qui consiste à ajouter deux caractères underscore __ devant un nom dattribut. Le langage bloquera alors toute tentative daccès à cet attribut (à moins dutiliser des moyens très alambiqués), ce qui revient à créer des attributs cachés, ou plutôt privés selon la terminologie classique en POO.

Utilisation du simple underscore

Voyons comment nous pourrions améliorer notre classe Rectangle par la première méthode:

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      # On teste la validité des paramètres:

      # Les quatre multiplications (ridicules) ci-dessous
      # ont une double et réelle utilité:
      # - elles déclenchent une erreur si les paramètres
      #   ne sont pas numériques (très bien !)
      # - elles assurent que les paramètres soient des
      #   flottants (un type numérique adapté à notre classe)
      x1 = 1.0*x1
      y1 = 1.0*y2
      x2 = 1.0*x2
      y2 = 1.0*y2

      if x1 > x2:
         # Les abscisses sont dans le mauvais ordre:
         # on les échange
         x1, x2 = x2, x1

      if y1 > y2:
         # Les ordonnées sont dans le mauvais ordre:
         # on les échange
         y1, y2 = y2, y1

      self._x1 = x1
      self._y1 = y1
      self._x2 = x2
      self._y2 = y2

   def x1(self):
      return self._x1

   def y1(self):
      return self._y1

   def x2(self):
      return self._x2

   def y2(self):
      return self._y2

   def longueur(self):
      return self._x2 - self._x1

   def hauteur(self):
      return self._y2 - self._y1

   def translation(self, dx, dy):
      self._x1 += dx
      self._x2 += dx
      self._y1 += dy
      self._y2 += dy

   def perimètre(self):
      return 2*(self.hauteur() + self.longueur())

   def aire(self):
      return self.hauteur()*self.longueur()

Les contraintes sont vérifiées lors de la création de lobjet. Certaines pourraient déclencher des erreurs (valeurs non numériques), dautres seront corrigées automatiquement (coordonnées mal ordonnées).

Quelques points importants:

  • Nous avons rajouté quatre méthodes permettant daccéder (en lecture mais pas en écriture) aux quatre attributs. Notons quil ny a aucun moyen de savoir si la méthode Rectangle.x1() couvre un attribut ou si elle est calculée par un autre moyen. Nous reviendrons sur ce point un peu plus loin.
  • La seule méthode qui permet de modifier les attributs cachés est translation(dx, dy), et elle ne peut pas casser les contraintes établies au moment de la construction: lutilisation de cette méthode est donc sûre.

Notons quil est toujours possible de ne pas sen tenir à linterface proposée, et de continuer à modifier les valeurs des attributs malgré les avertissements implicites du concepteur. Mais ce sera à nos risques et périls !

>>> r = Rectangle(3, 4, 12, 9)
>>> r._x1
3
>>> r._x1 = 21
>>> r.aire()
-45 

Utilisation du double underscore

En apparence, la modification semble légère:

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      # On teste la validité des paramètres:

      # Les quatre multiplications (ridicules) ci-dessous
      # ont une double et réelle utilité:
      # - elles déclenchent une erreur si les paramètres
      #   ne sont pas numériques (très bien !)
      # - elles assurent que les paramètres soient des
      #   flottants (un type numérique adapté à notre classe)
      x1 = 1.0*x1
      y1 = 1.0*y2
      x2 = 1.0*x2
      y2 = 1.0*y2

      if x1 > x2:
         # Les abscisses sont dans le mauvais ordre:
         # on les échange
         x1, x2 = x2, x1

      if y1 > y2:
         # Les ordonnées sont dans le mauvais ordre:
         # on les échange
         y1, y2 = y2, y1

      self.__x1 = x1
      self.__y1 = y1
      self.__x2 = x2
      self.__y2 = y2

   def x1(self):
      return self.__x1

   def y1(self):
      return self.__y1

   def x2(self):
      return self.__x2

   def y2(self):
      return self.__y2

   def longueur(self):
      return self.__x2 - self.__x1

   def hauteur(self):
      return self.__y2 - self.__y1

   def translation(self, dx, dy):
      self.__x1 += dx
      self.__x2 += dx
      self.__y1 += dy
      self.__y2 += dy

   def perimètre(self):
      return 2*(self.hauteur() + self.longueur())

   def aire(self):
      return self.hauteur()*self.longueur()

Les remarques formulées à la section précédente sont toujours dactualité. Mais il y a une différence fondamentale: il nest plus possible daccéder (facilement) aux attributs cachés:

>>> r = Rectangle(3, 4, 12, 9)
>>> r.__x1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Rectangle' object has no attribute '__x1'

Pourtant, la classe fonctionne parfaitement: les attributs sont accessibles de manière interne, par les méthodes définissant la classe, mais pas depuis lextérieur de celle-ci.

Nous ne détaillerons pas ici le mécanisme utilisé par python pour réaliser cela, ni comment le contourner.

À noter que la plupart des programmeurs python nutilisent pas ce mécanisme, préférant le précédent: le fait de cacher des attributs, ou plutôt de les rendre privés, nest pas dans un but de sécurité mais simplement de mise en garde contre une utilisation non souhaitée de lobjet. On part toujours du principe que les programmeurs feront preuve de bon sens.

En particulier, ces mécanismes (et ce, quel que soit le langage à objet) nont absolument pas pour objectif de protéger contre une tentative dintrusion par un hacker, par exemple. Ils noffrent absolument aucune protection dans ce domaine.

Changer limplémentation dun objet

Le fait de bien respecter les principes de lencapsulation offre une grande souplesse au programmeur. Notamment, il est possible de changer les détails de limplémentation dun objet sans en changer linterface. Ce faisant, tous les utilisateurs de notre classe pourront continuer à lutiliser sans le moindre changement.

Pourquoi voudrait-on changer limplémentation dun objet ?

  • une nouvelle implémentation pourrait être beaucoup plus simple du point de vue du programmeur.
  • une nouvelle implémentation pourrait utiliser un algorithme ou bien une structure de données beaucoup plus efficaces. Passer dun algorithme quadratique comme par exemple un tri par insertion ou par sélection à un algorithme plus efficace comme un tri par fusion peut changer lefficacité dune méthode de manière déterminante. Il nest pas toujours simple de trouver des algorithmes plus efficaces, et ils saccompagnent en général dune implémentation plus complexe, mais le gain en efficacité justifie quasiment systématiquement les efforts nécessaires (choix de lalgorithme, éventuellement nouvelles structures de données, implémentation, tests).

Voyons sur un exemple simple comment changer limplémentation de notre classe Rectangle sans en changer linterface. Il sagit ici dun pur exercice de style, nous ne gagnerons ni en clareté, ni en efficacité.

Linterface ne devant pas changer, nous avons donc lobligation de ne pas modifier les signatures des méthodes (constructeur compris) de notre classe. Nous choisissons par contre de représenter un rectangle par les coordonnées de son coin supérieur gauche et ses dimensions (longueur, hauteur).

class Rectangle:
   def __init__(self, x1, y1, x2, y2):
      # On teste la validité des paramètres:

      # Les quatre multiplications (ridicules) ci-dessous
      # ont une double et réelle utilité:
      # - elles déclenchent une erreur si les paramètres
      #   ne sont pas numériques (très bien !)
      # - elles assurent que les paramètres soient des
      #   flottants (un type numérique adapté à notre classe)
      x1 = 1.0*x1
      y1 = 1.0*y2
      x2 = 1.0*x2
      y2 = 1.0*y2

      if x1 > x2:
         # Les abscisses sont dans le mauvais ordre:
         # on les échange
         x1, x2 = x2, x1

      if y1 > y2:
         # Les ordonnées sont dans le mauvais ordre:
         # on les échange
         y1, y2 = y2, y1

      self.__x1 = x1
      self.__y1 = y1
      self.__longueur = x2 - x1
      self.__hauteur = y2 - y1

   def x1(self):
      return self.__x1

   def y1(self):
      return self.__y1

   def x2(self):
      return self.__x1 + self.__longueur

   def y2(self):
      return self.__y1 + self.__hauteur

   def longueur(self):
      return self.__longueur

   def hauteur(self):
      return self.__largeur

   def translation(self, dx, dy):
      self.__x1 += dx
      self.__x2 += dx

   def perimètre(self):
      return 2*(self.hauteur() + self.longueur())

   def aire(self):
      return self.hauteur()*self.longueur()

On peut constater que certaines méthodes sont plus simples, dautres légèrement plus compliquées. Les deux dernières méthodes nont strictement pas changé, puisquelles utilisent exclusivement linterface publique de la classe et non pas les attributs privés.

Le point important est que cette nouvelle implémentation de la classe Rectangle est parfaitement interchangeable avec la précédente.