C’est vers la fin des années 60 que fut inventé le premier langage utilisant le concept de classe: il s’agit du langage Simula 67 (les langages comportent souvent l’année de leur création dans leur nom). Ce langage n’est 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 n’est guère connu du grand public, mais il est l’inventeur du terme programmation orientée objet, ainsi que du concept d’interface graphique et de bureau, repris avec le succès que l’on 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 s’intitule pharo.
Le langage smalltalk est certainement le langage orienté objet le plus pur à l’heure actuelle: il n’autorise qu’un 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 l’arrivé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 à l’heure actuelle.
Les langages dynamiques, tels python (1980), sont pour la plupart fortement orientés objets.
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 d’entités:
C’est 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:
La plupart des langages à objets utilisent le concept de classe. Une classe est une définition abstraite d’un objet, une sorte de plan qui va servir ensuite à construire des représentations concrètes des objets.
D’autres 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 d’autres 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 qu’elle va représenter:
__init__
, appelée le constructeur, permet notamment d’initialiser l’objet et ses différents attributs;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.
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 qu’une unique méthode, le constructeur __init__(self, x1, x2, y1, y2)
. Le premier paramètre d’une méthode (conventionnelement toujours appelé self
, mais ce n’est pas une obligation) désigne toujours l’objet qui sera défini grâce à la classe. La syntaxe self.x1 = x1
permet ici de créer un nouvel attribut x1
pour l’objet 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 l’on 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 à l’aide de python tutor. Rien ne vaut une visualisation dynamique de l’exécution (cliquez sur le lien précédent !), mais voici quelques explications:
self
dans l’environnement d’exé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. self.
enlève toute ambiguïté. ma_liste.append(42)
: il s’agissait en fait de méthodes attachées à un objet particulier (en l’occurence 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 c’est tout. Notons que nous disposons néanmoins d’une nouvelle fonctionnalité intéressante: un objet rectangle, avec quatre attributs portant un nom (que l’on espère) intelligible et identifiable. C’est 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 l’interface de notre objet. Pour cela, nous allons devoir lui ajouter de nouvelles méthodes !
Quelle genre d’interface 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 n’avons pas reproduit le constructeur ici, puisqu’il n’a pas été modifié.
Nous disposons à présent de quatre nouvelles méthodes, qui permettent d’accé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 d’accé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 d’un appel. Concrètement, l’appel 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 l’objet r
, c’est-à-dire l’objet qui se trouve juste avant le point dans la syntaxe r.longueur()
.
Mais on peut tout aussi bien modifier les valeurs d’un ou plusieurs attributs.
Ajoutons une méthode translation(dx, dy)
qui prend pour paramètre les coordonnées d’un 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 d’ajouter 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)
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:
longueur()
et hauteur()
. Or, n’importe qui peut changer les attributs pour leur donner des valeurs ne respectant pas ces contraintes (voire, et c’est encore pire, des valeurs qui ne seraient même pas des nombres).Rectangle
est constamment conscient de la présence de ces quatre attributs. Cela peut paraître anodin, mais c’est 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 l’interface de l’objet. 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 s’astreindre à cacher les attributs. D’ailleurs, la plupart des langages objets imposent ce genre de restriction par défaut (c’est 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:
_
devant le nom de l’attribut. Cela n’empêche pas qui que ce soit d’accéder à cet attribut, mais cette convention est universellement respectée par les programmeurs python qui savent ainsi que cet attribut n’est pas destiné à être accédé à la main, et qu’il pourra être modifié voire disparaître à tout moment.__
devant un nom d’attribut. Le langage bloquera alors toute tentative d’accès à cet attribut (à moins d’utiliser 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.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 l’objet. Certaines pourraient déclencher des erreurs (valeurs non numériques), d’autres seront corrigées automatiquement (coordonnées mal ordonnées).
Quelques points importants:
Rectangle.x1()
couvre un attribut ou si elle est calculée par un autre moyen. Nous reviendrons sur ce point un peu plus loin.translation(dx, dy)
, et elle ne peut pas casser les contraintes établies au moment de la construction: l’utilisation de cette méthode est donc sûre.Notons qu’il est toujours possible de ne pas s’en tenir à l’interface 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
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 d’actualité. Mais il y a une différence fondamentale: il n’est plus possible d’accé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 l’exté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 n’utilisent 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, n’est pas dans un but de sécurité mais simplement de mise en garde contre une utilisation non souhaitée de l’objet. 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) n’ont absolument pas pour objectif de protéger contre une tentative d’intrusion par un hacker, par exemple. Ils n’offrent absolument aucune protection dans ce domaine.
Le fait de bien respecter les principes de l’encapsulation offre une grande souplesse au programmeur. Notamment, il est possible de changer les détails de l’implémentation d’un objet sans en changer l’interface. Ce faisant, tous les utilisateurs de notre classe pourront continuer à l’utiliser sans le moindre changement.
Pourquoi voudrait-on changer l’implémentation d’un objet ?
Voyons sur un exemple simple comment changer l’implémentation de notre classe Rectangle
sans en changer l’interface. Il s’agit ici d’un pur exercice de style, nous ne gagnerons ni en clareté, ni en efficacité.
L’interface ne devant pas changer, nous avons donc l’obligation 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, d’autres légèrement plus compliquées. Les deux dernières méthodes n’ont strictement pas changé, puisqu’elles utilisent exclusivement l’interface 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.