IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Une introduction à Python 3

Image non disponible


précédentsommairesuivant

7. La Programmation Orientée Objet

Image non disponible

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 :

Image non disponible

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.

Image non disponible
Figure 7.1 - Diagrammes de classe

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é :

 
Sélectionnez
>>> 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 :
 
Sélectionnez
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 :

 
Sélectionnez
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).
 
Sélectionnez
>>> 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 :

 
Sélectionnez
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.

 
Sélectionnez
>>> 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.

 
Sélectionnez
>>> 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 :

 
Sélectionnez
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().

 
Sélectionnez
>>> 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 :

 
Sélectionnez
>>> 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 ».

Image non disponible

L'implémentation Python utilisée est généralement l'intégration d'autres objets dans le constructeur de la classe conteneur :

 
Sélectionnez
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).

Image non disponible

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.

Image non disponible

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 :

 
Sélectionnez
>>> 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.

Image non disponible
Figure 7.5 - Un Ion « est-un » Atome
 
Sélectionnez
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
"""

précédentsommairesuivant
Les espaces de noms sont implémentés par des dictionnaires.
Pour plus de détails, consulter la documentation de référence du langage Python (The Python language reference) section 3, Data model, sous-section 3.3, Special method names.
Rappelons qu'il existe deux façons d'afficher un résultat : repr() et str(). La première est pour la machine, la seconde « pour l'utilisateur ».
Adapté de [1].

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Kordeo. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.