7. La Programmation Orientée Objet▲
La Programmation Orientée Objet :
- la POO permet de mieux modéliser la réalité en concevant des modèles d'objets, les classes ;
- ces classes permettent de construire des objets interactifs entre eux et avec le monde extérieur ;
- les objets sont créés indépendamment les uns des autres, grâce à l'encapsulation, mécanisme qui permet d'embarquer leurs propriétés ;
- les classes permettent d'éviter au maximum l'emploi des variables globales ;
- enfin les classes offrent un moyen économique et puissant de construire de nouveaux objets à partir d'objets préexistants.
7-1. Terminologie▲
7-1-1. Le vocabulaire de base de la POO▲
Une classe est équivalente à un nouveau type de données. On connaît déjà par exemple les classes list ou str et les nombreuses méthodes permettant de les manipuler, par exemple :
- [
3
,5
,1
].sort
(
) - casse.
upper
(
)
Un objet ou une instance est un exemplaire particulier d'une classe. Par exemple [3
, 5
, 1
] est une instance de la classe list et casse est une instance de la classe str.
Les objets ont généralement deux sortes d'attributs : les données nommées simplement attributs et les fonctions applicables appelées méthodes.
Par exemple un objet de la classe complex possède :
- deux attributs : imag et real ;
- plusieurs méthodes, comme conjugate(), abs()…
La plupart des classes encapsulent à la fois les données et les méthodes applicables aux objets. Par exemple un objet str contient une chaîne de caractères Unicode (les données) et de nombreuses méthodes.
On peut définir un objet comme une capsule contenant des attributs et des méthodes :
7-1-2. Notations UML de base▲
L'UML (Unified Modeling Language) est un langage graphique très répandu de conception des systèmes d'information.
UML propose une grande variété de diagrammes (classes, objets, états, activités, etc.). En première approche, le diagramme de classes est le plus utile pour concevoir les classes et leurs relations.
7-2. Classes et instanciation d'objets▲
7-2-1. L'instruction class▲
Cette instruction permet d'introduire la définition d'une nouvelle classe (c'est-à-dire d'un nouveau type de données).
class est une instruction composée. Elle comprend un en-tête (avec docstring) + corps indenté :
>>>
class
C:
... """Documentation de la classe C."""
... x =
23
Dans cet exemple, C est le nom de la classe (qui commence conventionnellement par une majuscule), et x est un attribut de classe, local à C.
7-2-2. L'instanciation et ses attributs▲
- Les classes sont des fabriques d'objets : on construit d'abord l'usine avant de produire des objets !
- On instancie un objet (c'est-à-dire qu'on le produit à partir de l'usine) en appelant le nom de sa classe comme s'il s'agissait d'une fonction :
class
C :
"""Documentation de la classe C."""
x =
23
# attribut de classe
y =
'un string'
z =
[1
, x, 3
, y]
# a est un objet de la classe C (ou une instance)
a =
C
(
)
# Affichage des attributs de l'instance a
print
(
a.x)
print
(
a.y)
print
(
a.z)
En Python (car c'est un langage dynamique comme Ruby, contrairement à C++ ou Java) il est possible d'ajouter de nouveaux attributs d'instance (ici le a.y =
44
) ou même de nouveaux attributs de classe (ici C.z =
6
).
Une variable définie au niveau d'une classe (comme x dans la classe C) est appelée attribut de classe et est partagée par tous les objets instances de cette classe.
Une variable définie au niveau d'un objet (comme y dans l'objet a) est appelée attribut d'instance et est liée uniquement à l'objet pour lequel elle est définie.
7-2-3. Retour sur les espaces de noms▲
On a déjà vu les espaces de noms(23) locaux (lors de l'appel d'une fonction), globaux (liés aux modules) et internes (fonctions standard), ainsi que la règle « Local Global Interne » (cf. section 5.3Espaces de noms) qui définit dans quel ordre ces espaces sont parcourus pour résoudre un nom.
Les classes ainsi que les objets instances définissent de nouveaux espaces de noms, et il y a là aussi des règles pour résoudre les noms :
- les classes peuvent utiliser les variables définies au niveau principal, mais elles ne peuvent pas les modifier ;
- les instances peuvent utiliser les variables définies au niveau de la classe, mais elles ne peuvent pas les modifier (pour cela elles sont obligées de passer par l'espace de noms de la classe, par exemple C.x
=
3
).
Exemple de masquage d'attribut :
class
C :
"""Documentation de la classe C."""
x =
23
# attribut de classe
a =
C
(
) # a est un objet de la classe C (ou une instance)
a.x # 23 : affiche la valeur de l'attribut de l'instance a
a.x =
12
# modifie son attribut d'instance (attention#8230;)
a.x # 12
C.x # 23 mais l'attribut de classe est inchangé
C.z =
6
# z : nouvel attribut de classe
a.y =
44
# y : nouvel attribut de l'instance a
b =
C
(
) # b est un autre objet de la classe C (une autre instance)
b.x # 23 : b connaît bien son attribut de classe, mais#8230;
b.y # ... b n'a pas d'attribut y !
"""
Retraçage (dernier appel le plus récent) :
Fichier"/home/ipy/Phi/3-corps/ch07/src/7_025.py", ligne 14, dans 0
builtins.AttributeError : 'C' object has no attribute 'y'
"""
7-2-3-a. Recherche des noms▲
- Noms non qualifiés (exemple dimension) l'affectation crée ou change le nom dans la portée locale courante. Ils sont cherchés suivant la règle LGI.
- Noms qualifiés (exemple dimension.hauteur) l'affectation crée ou modifie l'attribut dans l'espace de noms de l'objet. Un attribut est cherché dans l'objet, puis dans toutes les classes dont l'objet dépend (mais pas dans les modules).
>>>
v =
5
>>>
class
C:
... x =
v +
3
# utilisation d'une variable globale dans la définition de classe
... y =
x +
1
# recherche dans l'espace de noms de la classe lors de la définition
...
>>>
a =
C
(
)
>>>
a.x # utilisation sans modification de la variable de classe en passant par l'objet
8
>>>
a.x =
2
# création d'une variable d'instance pour l'objet a
>>>
a.x
2
>>>
C.x # la variable de classe n'est pas modifiée
8
>>>
C.x =
-
1
# on peut modifier la variable de classe en passant par l'espace de noms de la classe
>>>
C.x
-
1
À chaque création d'une classe C, Python lui associe un dictionnaire (de nom C.__dict__) contenant un ensemble d'informations. L'exemple suivant affiche le dictionnaire lié à la classe C :
In [1]: class C:
...: """Une classe simple."""
...: x = 2
...: y = 5
...:
In [2]: C.__dict__
Out[2]: mappingproxy({'x': 2, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'C' objects>, '__dict__': <attribute '__
In [3]: for key in C.__dict__.keys():
...: print(key)
...:
x
__module__
__weakref__
__dict__
y
__doc__
In [4]: C.__dict__['__doc__']
Out[4]: 'Une classe simple.'
In [5]: C.__dict__['y']
Out[5]: 5
In [6]: a = C()
In [7]: a.__class__
Out[7]: __main__.C
In [8]: a.__doc__ # notation compacte
Out[8]: 'Une classe simple.'
In [9]: a.y # notation compacte
Out[9]: 5
Notons également l'instruction dir() qui fournit tous les noms définis dans l'espace de noms, méthodes et variables membres compris.
7-3. Méthodes▲
Une méthode s'écrit comme une fonction du corps de la classe avec un premier paramètre self obligatoire, où self représente l'objet sur lequel la méthode sera appliquée.
Autrement dit self est la référence d'instance.
>>>
class
C: # x et y : attributs de classe
... x =
23
... y =
x +
5
... def
affiche
(
self): # méthode affiche()
... self.z =
42
# attribut d'instance
... print
(
C.y) # dans une méthode, on qualifie un attribut de classe,
... print
(
self.z) #, mais pas un attribut d'instance
...
>>>
obj =
C
(
)
>>>
obj.affiche
(
)
28
42
7-4. Méthodes spéciales▲
Beaucoup de classes offrent des caractéristiques supplémentaires comme la concaténation des chaînes en utilisant simplement l'opérateur +. Ceci est obtenu grâce aux méthodes spéciales. Par exemple l'opérateur + est utilisable, car la classe des chaînes a redéfini la méthode spéciale __add__().
Ces méthodes portent des noms prédéfinis, précédés et suivis de deux caractères de soulignement.
Elles servent :
- à initialiser l'objet instancié ;
- à modifier son affichage ;
- à surcharger ses opérateurs ;
- …
7-4-1. L'initialisateur▲
Lors de l'instanciation d'un objet, la structure de base de l'objet est créée en mémoire, et la méthode __init__ est automatiquement appelée pour initialiser l'objet. C'est typiquement dans cette méthode spéciale que sont créés les attributs d'instance avec leur valeur initiale.
>>>
class
C:
... def
__init__
(
self, n):
... self.x =
n # initialisation de l'attribut d'instance x
...
>>>
une_instance =
C
(
42
) # paramètre obligatoire, affecté à n
>>>
une_instance.x
42
C'est une procédure automatiquement invoquée lors de l'instanciation : elle ne retourne aucune valeur.
7-4-2. Surcharge des opérateurs▲
La surcharge permet à un opérateur de posséder un sens différent suivant le type de ses opérandes.
Par exemple, l'opérateur + permet :
x =
7
+
9
# addition entière
s =
'ab'
+
'cd'
# concaténation
Python possède des méthodes de surcharge pour :
- tous les types (__call__, __str__…) ;
- les nombres (__add__, __div__…) ;
- les séquences (__len__, __iter__…).
Soient deux instances, obj1 et obj2, les méthodes spéciales suivantes permettent d'effectuer les opérations arithmétiques courantes(24) :
Nom |
Méthode spéciale |
Utilisation |
---|---|---|
opposé |
__neg__ |
-obj1 |
addition |
__add__ |
obj1 + obj2 |
soustraction |
__sub__ |
obj1 - obj2 |
multiplication |
__mul__ |
obj1 * obj2 |
division |
__div__ |
obj1 / obj2 |
division entière |
__floordiv__ |
obj1 // obj2 |
Voir l'Abrégé dense, les méthodes spéciales (cf. chapitre 16Mémento des bases de Python 3).
7-4-3. Exemple de surcharge▲
Dans l'exemple suivant, nous surchargeons l'opérateur d'addition pour le type Vecteur2D.
Nous surchargeons également la méthode spéciale __str__ utilisée pour l'affichage(25) par print().
>>>
class
Vecteur2D:
... def
__init__
(
self, x0, y0):
... self.x =
x0
... self.y =
y0
... def
__add__
(
self, second): # addition vectorielle
... return
Vecteur2D
(
self.x +
second.x, self.y +
second.y)
... def
__str__
(
self): # affichage d'un Vecteur2D
... return
"Vecteur({:g}, {:g})"
.format
(
self.x, self.y)
...
>>>
v1 =
Vecteur2D
(
1.2
, 2.3
)
>>>
v2 =
Vecteur2D
(
3.4
, 4.5
)
>>>
>>>
print
(
v1 +
v2)
Vecteur
(
4.6
, 6.8
)
7-5. Héritage et polymorphisme▲
Un avantage décisif de la POO est qu'une classe Python peut toujours être spécialisée en une classe fille qui hérite alors de tous les attributs (données et méthodes) de sa super classe. Comme tous les attributs peuvent être redéfinis, une méthode de la classe fille et de la classe mère peut posséder le même nom, mais effectuer des traitements différents (surcharge) et l'objet s'adaptera dynamiquement, dès l'instanciation. En proposant d'utiliser un même nom de méthode pour plusieurs types d'objets différents, le polymorphisme permet une programmation beaucoup plus générique. Le développeur n'a pas à savoir, lorsqu'il programme une méthode, le type précis de l'objet sur lequel la méthode va s'appliquer. Il lui suffit de savoir que cet objet implémentera la méthode.
7-5-1. Héritage et polymorphisme▲
L'héritage est le mécanisme qui permet de se servir d'une classe préexistante pour en créer une nouvelle qui possédera des fonctionnalités supplémentaires ou différentes.
Le polymorphisme par dérivation est la faculté pour deux méthodes (ou plus) portant le même nom, mais appartenant à des classes héritées distinctes d'effectuer un travail différent. Cette propriété est acquise par la technique de la surcharge.
7-5-2. Exemple d'héritage et de polymorphisme▲
Dans l'exemple suivant, la classe QuadrupedeDebout hérite de la classe mère Quadrupede, et la méthode piedsAuContactDuSol() est polymorphe :
>>>
class
Quadrupede:
... def
piedsAuContactDuSol
(
self):
... return
4
...
>>>
class
QuadrupedeDebout
(
Quadrupede):
... def
piedsAuContactDuSol
(
self):
... return
2
...
>>>
chat =
Quadrupede
(
)
>>>
chat.piedsAuContactDuSol
(
)
4
>>>
homme =
QuadrupedeDebout
(
)
>>>
homme.piedsAuContactDuSol
(
)
2
7-6. Notion de conception orientée objet▲
Suivant les relations que l'on va établir entre les objets de notre application, on peut concevoir nos classes de deux façons possibles en utilisant l'association ou la dérivation.
Bien sûr, ces deux conceptions peuvent cohabiter, et c'est souvent le cas !
7-6-1. Association▲
Une association représente un lien unissant les instances de classe. Elle repose sur la relation « a-un » ou « utilise-un ».
L'implémentation Python utilisée est généralement l'intégration d'autres objets dans le constructeur de la classe conteneur :
class
Point :
def
__init__
(
self, x, y) :
self.px, self.py =
x, y
class
Segment :
"""Classe conteneur utilisant la classe Point."""
def
__init__
(
self, x1, y1, x2, y2) :
self.orig =
Point
(
x1, y1)
self.extrem =
Point
(
x2, y2)
def
__str__
(
self) :
return
(
"Segment : [({ :g}, { :g}), ({ :g}, { :g})]"
.format
(
self.orig.px, self.orig.py, self.extrem.px, self.extrem.py))
s =
Segment
(
1.0
, 2.0
, 3.0
, 4.0
)
print
(
s) # Segment : [(1, 2), (3, 4)]
7-6-1-a. Agrégation▲
Une agrégation est une association non symétrique entre deux classes (l'agrégat et le composant).
7-6-1-b. Composition▲
Une composition est un type particulier d'agrégation dans laquelle la vie des composants est liée à celle de l'agrégat.
La disparition de l'agrégat Commune entraîne la disparition des composants Services et Conseil_Municipal alors que Village n'en dépend pas.
7-6-2. Dérivation▲
La dérivation décrit la création de sous-classes par spécialisation. Elle repose sur la relation « est-un ».
On utilise dans ce cas le mécanisme de l'héritage.
L'implémentation Python utilisée est généralement l'appel à l'initialisateur de la classe parente dans l'initialisateur de la classe dérivée (utilisation de la fonction super()).
Dans l'exemple suivant, un Carre « est-un » Rectangle particulier pour lequel on appelle l'initialisateur de la classe mère avec les paramètres longueur=cote et largeur=cote :
>>>
class
Rectangle:
... def
__init__
(
self, longueur=
30
, largeur=
15
):
... self.L, self.l =
longueur, largeur
... self.nom =
"rectangle"
... def
__str__
(
self):
... return
"nom : {}"
.format
(
self.nom)
...
>>>
class
Carre
(
Rectangle): # héritage simple
... """Sous-classe spécialisée de la super-classe Rectangle."""
... def
__init__
(
self, cote=
20
):
... # appel au constructeur de la super-classe de Carre :
... super(
).__init__
(
cote, cote)
... self.nom =
"carré"
# surcharge d'attribut
...
>>>
r =
Rectangle
(
)
>>>
c =
Carre
(
)
>>>
print
(
r)
nom : rectangle
>>>
print
(
c)
nom : carré
7-7. Un exemple complet▲
Le script suivant(26) propose un modèle simplifié d'atome et d'ion.
La variable de classe table liste les 10 premiers éléments du tableau de Mendeleïev.
class
Atome :
"""atomes simplifiés (les 10 premiers éléments)."""
table =
[None
, (
'hydrogene'
, 0
), (
'helium'
, 2
), (
'lithium'
, 4
),
(
'beryllium'
, 5
), (
'bore'
, 6
), (
'carbone'
, 6
), (
'azote'
, 7
),
(
'oxygene'
, 8
), (
'fluor'
, 10
), (
'neon'
, 10
)]
def
__init__
(
self, nat) :
"""le numéro atomique détermine les nombres de protons, d'électrons et de neutrons"""
self.np, self.ne =
nat, nat # nat = numéro atomique
self.nn =
Atome.table[nat][1
]
def
affiche
(
self) :
print
(
)
print
(
"Nom de l'élément :"
, Atome.table[self.np][0
])
print
(
"
%s
protons,
%s
électrons,
%s
neutrons"
%
(
self.np, self.ne, self.nn))
class
Ion
(
Atome) : # Ion hérite d'Atome
"""Les ions sont des atomes qui ont gagné ou perdu des électrons"""
def
__init__
(
self, nat, charge) :
"""le numéro atomique et la charge électrique déterminent l'ion"""
super(
).__init__
(
nat)
self.ne =
self.ne -
charge # surcharge
self.charge =
charge
def
affiche
(
self) : # surcharge
Atome.affiche
(
self)
print
(
"Particule électrisée. Charge ="
, self.charge)
# Programme principal =========================================================
a1 =
Atome
(
5
)
a2 =
Ion
(
3
, 1
)
a3 =
Ion
(
8
, -
2
)
a1.affiche
(
)
a2.affiche
(
)
a3.affiche
(
)
"""
Nom de l'élément : bore
5 protons, 5 électrons, 6 neutrons
Nom de l'élément : lithium
3 protons, 2 électrons, 4 neutrons
Particule électrisée. Charge = 1
Nom de l'élément : oxygene
8 protons, 10 électrons, 8 neutrons
Particule électrisée. Charge = -2
"""