Classes et objets#

Cette page s’appuie sur le livre de Gérard Swinnen « Apprendre à programmer avec Python 3 » disponible sous licence CC BY-NC-SA 2.0. L’introduction à la programmation orientée objet est inspirée par le livre de Claude Delannoy « Programmer en Java » (Eyrolles) que vous êtes invités à consulter si vous souhaitez découvrir le langage Java.

Python est un langage qui permet la Programmation Orientée Objet (POO).

Brève introduction à la Programmation Orientée Objet#

Nous avons vu plusieurs types de base en Python (int pour les entiers, float pour les flottants, str pour les chaînes de caractères, etc.). La notion de classe va en quelque sorte nous permettre de généraliser la notion de « type » afin de créer de nouvelles structures de données.

Une classe définit des attributs et des méthodes. Par exemple, imaginons une classe Voiture qui servira à créer des objets qui sont des voitures. Cette classe va pouvoir définir un attribut couleur, un attribut vitesse, etc. Ces attributs correspondent à des propriétés qui peuvent exister pour une voiture. La classe Voiture pourra également définir une méthode rouler(). Une méthode correspond en quelque sorte à une action, ici l’action de rouler peut être réalisée pour une voiture.

_images/classe_voiture.svg

Si on imagine une classe Avion, elle pourra définir une méthode voler(). Elle pourra aussi définir une méthode rouler(). Par contre, la classe Voiture n’aura pas de méthode voler() car une voiture ne peut pas voler. De même, la classe Avion pourra avoir un attribut altitude mais ce ne sera pas le cas pour la classe Voiture.

Après avoir présenté la notion de classe, nous allons voir la notion d”objet. On dit qu’un objet est une instance de classe. Si on revient à la classe Voiture, nous pourrons avoir plusieurs voitures qui seront chacune des instances bien distinctes. Par exemple, la voiture de Jonathan, qui est de couleur rouge avec une vitesse de 30 km/h, est une instance de la classe Voiture, c’est un objet. De même, la voiture de Denis, qui est de couleur grise avec une vitesse de 50 km/h, est un autre objet. Nous pouvons donc avoir plusieurs objects pour une même classe, en particulier ici deux objets (autrement dit : deux instances de la même classe). Chacun des objets a des valeurs qui lui sont propres pour les attributs.

Les notions de classe et d’objet#

Définition d’une classe Point#

Voici comment définir une classe appelée ici Point.

class Point:
    "Definition d'un point geometrique"

Par convention en Python, le nom identifiant une classe (qu’on appelle aussi son identifiant) débute par une majuscule. Ici Point débute par un P majuscule.

Création d’un objet de type Point#

Point()

Ceci crée un objet de type Point. En POO, on dit que l’on crée une instance de la classe Point.

Une phrase emblématique de la POO consiste à dire qu”un objet est une instance de classe.

Il faut bien noter que pour créer une instance, on utilise le nom de la classe suivi de parenthèses. Nous verrons par la suite qu’il peut y avoir des arguments entre ces parenthèses.

Affectation à une variable de la référence à un objet

Nous venons de définir une classe Point. Nous pouvons dès à présent nous en servir pour créer des objets de ce type, par instanciation. Créons par exemple un nouvel objet et mettons la référence à cet objet dans la variable p :

>>> p = Point()

Avertissement

Comme pour les fonctions, lors de l’appel à une classe dans une instruction pour créer un objet, il faut toujours indiquer des parenthèses (même si aucun argument n’est transmis). Nous verrons un peu plus loin que ces appels peuvent se faire avec des arguments (voir la notion de constructeur).

Remarquez bien cependant que la définition d’une classe ne nécessite pas de parenthèses (contrairement à ce qui de règle lors de la définition des fonctions), sauf si nous souhaitons que la classe en cours de définition dérive d’une autre classe préexistante (ceci sera expliqué plus loin).

Nous pouvons dès à présent effectuer quelques manipulations élémentaires avec notre nouvel objet dont la référence est dans p.

Exemple

>>> print(p)
<__main__.Point instance at 0x012CAF30>

Le message renvoyé par Python indique que p contient une référence à une instance de la classe Point, qui est définie elle-même au niveau principal du programme. Elle est située dans un emplacement bien déterminé de la mémoire vive, dont l’adresse apparaît ici en notation hexadécimale.

>>> print(p.__doc__)
Definition d'un point geometrique

On peut noter que les chaînes de documentation de divers objets Python sont associées à l’attribut prédéfini __doc__:.

Exemple avec deux objets

a = Point()
b = Point()

La variable a va contenir une référence à un objet.

>>> print(a)
<__main__.Point instance at 0x012CADC8>

De même b va contenir une référence à un autre objet.

>>> print(b)
<__main__.Point instance at 0x012CAF08>

Nous avons ici 2 instances de la classe Point (2 objets) :

  • la première à laquelle on fait référence au moyen de la variable a,

  • la seconde à laquelle on fait référence au moyen de la variable b.

On fait bien ici la distinction entre classe et objet. Ici nous avons une seule classe Point, et deux objets de type Point.

Définition des attributs#

class Point:
    "Definition d'un point geometrique"

p = Point()
p.x = 1
p.y = 2
print("p : x =", p.x, "y =", p.y)

Exécuter

L’objet dont la référence est dans p possède deux attributs : x et y.

_images/2014-11-22_13-30-47.png

La syntaxe pour accéder à un attribut est la suivante : on va utiliser la variable qui contient la référence à l’objet et on va mettre un point . puis le nom de l’attribut.

Exemple

class Point:
    "Definition d'un point geometrique"

a = Point()
a.x = 1
a.y = 2
b = Point()
b.x = 3
b.y = 4
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)

Exécuter

On a 2 instances de la classe Point, c’est-à-dire 2 objets de type Point. Pour chacun d’eux, les attributs prennent des valeurs qui sont propres à l’instance.

_images/2014-11-22_09-47-40.png

Distinction entre variable et objet

L’exemple suivant montre bien la distinction entre variable et objet :

class Point:
    "Definition d'un point geometrique"

a = Point()
a.x = 1
a.y = 2
b = a
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
a.x = 3
a.y = 4
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)

Exécuter

_images/2014-11-22_09-56-48.png

Ici les variables a et b font référence au même objet. En effet, lors de l’affectation b = a, on met dans la variable b la référence contenue dans la variable a. Par conséquent, toute modification des valeurs des attributs de l’objet dont la référence est contenue dans a entraîne une modification pour b.

Avertissement

Par abus de langage on parlera parfois de l’objet a alors qu’il s’agira en fait de l’objet auquel a fait référence.

Définition des méthodes#

class Point:
    def deplace(self, dx, dy):
        self.x = self.x + dx
        self.y = self.y + dy

Cette classe possède une méthode : deplace().

Pour définir une méthode, il faut :

  1. indiquer son nom (ici deplace()).

  2. indiquer les arguments entre des parenthèses. Le premier argument d’une méthode doit être self.

Pour accéder aux méthodes d’un objet, on indique :

  1. le nom de la variable qui fait référence à cet objet

  2. un point

  3. le nom de la méthode

a.deplace(3, 5)

Avertissement

Lors de l’appel de la méthode, le paramètre self n’est pas utilisé et la valeur qu’il prend est la référence à l’objet. Il y a donc toujours un paramètre de moins que lors de la définition de la méthode.

Exemple

class Point:
    def deplace(self, dx, dy):
        self.x = self.x + dx
        self.y = self.y + dy

a = Point()
a.x = 1
a.y = 2
print("a : x =", a.x, "y =", a.y)
a.deplace(3, 5)
print("a : x =", a.x, "y =", a.y)

Exécuter

La notion de constructeur#

Si lors de la création d’un objet nous voulons qu’un certain nombre d’actions soit réalisées (par exemple une initialisation), nous pouvons utiliser un constructeur.

Un constructeur n’est rien d’autre qu’une méthode, sans valeur de retour, qui porte un nom imposé par le langage Python : __init__(). Ce nom est constitué de init entouré avant et après par __ (deux fois le symbole underscore _, qui est le tiret sur la touche 8). Cette méthode sera appelée lors de la création de l’objet. Le constructeur peut disposer d’un nombre quelconque de paramètres, éventuellement aucun.

Exemple sans paramètre

class Point:
    def __init__(self):
        self.x = 0
        self.y = 0

a = Point()
print("a : x =", a.x, "y =", a.y)
a.x = 1
a.y = 2
print("a : x =", a.x, "y =", a.y)

Exécuter

Dans cet exemple, nous avons pu définir des valeurs par défaut pour les attributs grâce au constructeur.

Exemple avec paramètres

class Point:
    def __init__(self, abs, ord):
        self.x = abs
        self.y = ord

a = Point(1, 2)
print("a : x =", a.x, "y =", a.y)

Exécuter

Autre exemple avec paramètres

Dans l’exemple suivant, on utilise les mêmes noms pour les paramètres du constructeur et les attributs. Ceci ne pose pas de problème car ces variables ne sont pas dans le même espace de noms. Les paramètres du constructeur sont des variables locales, comme c’est habituellement le cas pour une fonction. Les attributs de l’objet sont eux dans l’espace de noms de l’instance. Les attributs se distinguent facilement car ils ont self devant.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

a = Point(1, 2)
print("a : x =", a.x, "y =", a.y)

Exécuter

Exemple complet

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def deplace(self, dx, dy):
        self.x = self.x + dx
        self.y = self.y + dy

a = Point(1, 2)
b = Point(3, 4)
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)
a.deplace(3, 5)
b.deplace(-1, -2)
print("a : x =", a.x, "y =", a.y)
print("b : x =", b.x, "y =", b.y)

Exécuter

Exercice

Modifier le programme de façon à ajouter deux autres objets de type Point. Ils seront référencés par des variables c et d.

Exercice

Définir une classe Point3D qui sera analogue à la classe Point mais pour des points dans l’espace à 3 dimensions. Créer deux objets de type Point3D qui seront référencés par les variables a3D et b3D. Initialiser ces points et afficher leurs coordonnées x, y, z.

La notion d’encapsulation#

Le concept d”encapsulation est un concept très utile de la POO. Il permet en particulier d’éviter une modification par erreur des données d’un objet. En effet, il n’est alors pas possible d’agir directement sur les données d’un objet ; il est nécessaire de passer par ses méthodes qui jouent le rôle d’interface obligatoire.

Définition d’attributs privés#

On réalise la protection des attributs de notre classe Point grâce à l’utilisation d’attributs privées. Pour avoir des attributs privés, leur nom doit débuter par __ (deux fois le symbole underscore _, qui est le tiret sur la touche 8).

class Point:
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

Il n’est alors plus possible de faire appel aux attributs __x et __y depuis l’extérieur de la classe Point.

>>> p = Point(1, 2)
>>> p.__x

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in
    p.__x
AttributeError: Point instance has no attribute '__x'

Il faut donc disposer de méthodes qui vont permettre par exemple de modifier ou d’afficher les informations associées à ces variables.

class Point:
    def __init__(self, x, y):
        self.__x = x
        self.__y = y

    def deplace(self, dx, dy):
        self.__x = self.__x + dx
        self.__y = self.__y + dy

    def affiche(self):
        print("abscisse =", self.__x, "ordonnee =", self.__y)

a = Point(2, 4)
a.affiche()
a.deplace(1, 3)
a.affiche()

Exécuter

Accesseurs et mutateurs#

Parmi les différentes méthodes que comporte une classe, on a souvent tendance à distinguer :

  • les constructeurs ;

  • les accesseurs (en anglais accessor) qui fournissent des informations relatives à l’état d’un objet, c’est-à-dire aux valeurs de certains de ses attributs (généralement privés) sans les modifier ;

  • les mutateurs (en anglais mutator) qui modifient l’état d’un objet, donc les valeurs de certains de ses attributs.

On rencontre souvent l’utilisation de noms de la forme get_XXXX() pour les accesseurs et set_XXXX() pour les mutateurs, y compris dans des programmes dans lesquels les noms de variable sont francisés. Par exemple, pour la classe Point sur laquelle nous avons déjà travaillé on peut définir les méthodes suivantes :

Exemple :

class Point:
    def __init__(self, x, y):
        self.set_x(x)
        self.set_y(y)

    def get_x(self):
        return self.__x

    def set_x(self, x):
        self.__x = x

    def get_y(self):
        return self.__y

    def set_y(self, y):
        self.__y = y

a = Point(3, 7)
print("a : abscisse =", a.get_x())
print("a : ordonnee =", a.get_y())
a.set_x(6)
a.set_y(10)
print("a : abscisse =", a.get_x())
print("a : ordonnee =", a.get_y())

Exécuter

L’utilisation d’un mutateur pour fixer la valeur d’un attribut autorise la possibilité d’effectuer un contrôle sur les valeurs de l’attribut. Par exemple, il serait possible de n’autoriser que des valeurs positives pour les attributs privés __x et __y.

Notez qu’il n’est pas toujours prudent de prévoir une méthode d’accès pour chacun des attributs privés d’un objet. En effet, il ne faut pas oublier qu’il doit toujours être possible de modifier l’implémentation d’une classe de manière transparente pour son utilisateur.

Exemple avec le décorateur @property

Il existe en Python une autre approche pour gérer ce type de situation. Elle utilise le décorateur @property.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, x):
        self._x = x

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, y):
        self._y = y

a = Point(3, 7)
print("a : abscisse =", a.x)
print("a : ordonnee =", a.y)
a.x = 6
a.y = 10
print("a : abscisse =", a.x)
print("a : ordonnee =", a.y)

Exécuter

On utilise des attributs précédés par un underscore. Il s’agit d’une convention pour indiquer que ces attributs ne doivent pas être utilisés en dehors de la classe et qu’il faut utiliser les accesseurs et mutateurs pour les manipuler.

Exercice

Définir une classe Point3D analogue à la classe Point mais pour des points dans l’espace à 3 dimensions.

Attributs et méthodes de classe#

Attributs de classe#

Exemple :

class A:
    nb = 0

    def __init__(self, x):
        print("creation objet de type A")
        self.x = x
        A.nb = A.nb + 1

print("A : nb = ", A.nb)
print("Partie 1")
a = A(3)
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("Partie 2")
b = A(6)
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("b : x = ", b.x, " nb = ", b.nb)
c = A(8)
print("Partie 3")
print("A : nb = ", A.nb)
print("a : x = ", a.x, " nb = ", a.nb)
print("b : x = ", b.x, " nb = ", b.nb)
print("c : x = ", c.x, " nb = ", c.nb)

Exécuter

Méthodes de classe#

Exemple :

class A:
    nb = 0

    def __init__(self):
        print("creation objet de type A")
        A.nb = A.nb + 1
        print("il y en a maintenant ", A.nb)

    @classmethod
    def get_nb(cls):
        return A.nb

print("Partie 1 : nb objets = ", A.get_nb())
a = A()
print("Partie 2 : nb objets = ", A.get_nb())
b = A()
print("Partie 3 : nb objets = ", A.get_nb())

Exécuter

Pour créer une méthode de classe, il faut la faire précéder d’un « décorateur » : @classmethod

Le premier argument de la méthode de classe doit être cls.