deutsch     english    français     Imprimer

 

7.2 CLASSES ET OBJETS

 

 

INTRODUCTION

.
 

Vous avez déjà pu vous familiariser avec les notions essentielles de la programmation orientée objets (POO) et compris qu’il serait très difficile de développer un jeu vidéo en Python sans recourir à cette technique. Il est de ce fait capital de continuer à comprendre ces notions ainsi que leur implémentation en Python de manière plus systématique.


CONCEPTS DE PROGRAMMATION:
Héritage, hiérarchie de classes, redéfinition (overriding), relation « is-a », héritage multiple

 

 

VARIABLES D’INSTANCE

 

Les animaux se prêtent bien à être représentés par des objets. On commence par définir une classe Animal qui affiche l’image de l’animal approprié dans l’arrière-fond de la fenêtre de jeu. Lors de la construction d’une instance de cette classe, on spécifie au constructeur le chemin vers le fichier image à utiliser de sorte que le méthode showMe() soit capable d’afficher l’image grâce aux méthodes de dessin de la classe GGBackground.

Les animaux se prêtent bien à être représentés par des objets. On commence par définir une classe Animal qui affiche l’image de l’animal approprié dans l’arrière-fond de la fenêtre de jeu. Lors de la construction d’une instance de cette classe, on spécifie au constructeur le chemin vers le fichier image à utiliser de sorte que le méthode showMe() soit capable d’afficher l’image grâce aux méthodes de dessin de la classe GGBackground. Le constructeur qui reçoit ce chemin vers le fichier image doit le sauver dans une variable de sorte que toutes les autres méthodes de la classe puissent y avoir accès ultérieurement. Une telle variable est un attribut d’instance ou variable d’instance. En Python, les variables d’instance sont préfixées au sein des objets par self et sont créées en mémoire lorsqu’on leur affecte pour la première fois une valeur. Comme nous l’avons déjà mentionné, le constructeur porte le nom spécial __init__(avec deux caractères de soulignement « underscore » avant et après). Aussi bien le constructeur que les méthodes de la classe doivent impérativement être définis avec self comme premier paramètre formel, sans quoi des erreurs apparaîtront lors de leur utilisation.

On commence donc par définir le constructeur comme suit :

def __init__(self, imgPath:

de même qu’une méthode 

def showMe(self, x, y:

Une fois que l’on a créé un objet myAnimal avec
myAnimal = Animal(bildpfad)
Une fois que l’on a créé un objet myAnimal avec
myAnimal.showMe(x, y)
 

Il est particulièrement indiqué de recourir à la POO lorsque l’on utilise plusieurs objets de la même classe. Pour bien mettre ceci en évidence, le programme ci-dessous fait apparaitre un nouvel animal lors de chaque clic de souris.

from gamegrid import *

# ---------------- class Animal ----------------
class Animal():
    def __init__(self, imgPath):
        self.imagePath = imgPath  # Instance variable
    def showMe(self, x, y):  # Method definition
         bg.drawImage(self.imagePath, x, y) 

def pressCallback(e):
    myAnimal = Animal("sprites/animal.gif") # Object creation
    myAnimal.showMe(e.getX(), e.getY())  # Method call

makeGameGrid(600, 600, 1, False, mousePressed = pressCallback)
setBgColor(Color.green)
show()
doRun()
bg = getBg()
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Les propriétés ou attributs d’un objet sont définis au moyen des variables d'instance qui prennent des valeurs distinctes pour chaque instance de la classe. Pour accéder à une variable d’instance attribut depuis l’une des méthodes de l’instance, on doit préfixer son nom par le fameux self, ce qui donne self.attribut. On peut aussi y accéder depuis l’extérieur de la classe en préfixant son nom d’une instance particulière comme monInstance.attribut.

Une classe peut également accéder aux variables et fonctions du programme principal présentes dans l’espace de noms global. Elle a ainsi par exemple accès à toutes les méthodes de la classe GameGrid ou à l’arrière-plan de la fenêtre de jeu référencé par la variable bg.

Dans le cas où l’objet ne nécessite pas d’initialisation particulière, la définition du constructeur peut être omise. Dans le programme suivant, au lieu de passer l’image de sprite au constructeur en tant qu’argument, on peut utiliser la variable imagePath pour se passer du constructeur.

from gamegrid import *
import random

# ---------------- class Animal ----------------
class Animal():
    def showMe(self, x, y):
         bg.drawImage(imagePath, x, y) 

def pressCallback(e):
    myAnimal = Animal()
    myAnimal.showMe(e.getX(), e.getY())

imagePath = "sprites/animal.gif"
makeGameGrid(600, 600, 1, False, mousePressed = pressCallback)
setBgColor(Color.green)
show()
doRun()
bg = getBg()

 

 

HÉRITAGE, AJOUT DE MÉTHODES

 

Les hiérarchies de classes sont créées par dérivation de classes ou héritage, ce qui permet d’ajouter à une classe existante de nouvelles méthodes et attributs. Les instances de la classe dérivée sont également considérées des instances de la classe parente (également appelée classe de base ou super classe) et peuvent de ce fait utiliser toutes les méthodes et attributs de la classe parente. Du point de vue de la classe dérivée, c’est comme si ces méthodes ou attributs hérités étaient directement définis dans la classe dérivée elle-même.

Concrètement, un animal de compagnie possède tous les attributs et comportements d’un animal mais dispose en plus d’un nom qu’il peut afficher avec la méthode tell(). De ce fait, on définit une classe Pet (Animal de compagnie) dérivée de la classe Animal. Comme on veut pouvoir spécifier le nom de chaque animal de compagnie individuellement lors de sa création, on équipe le constructeur d’un paramètre formel supplémentaire name servant à initialiser la variable d’instance self.name propre à chaque animal de compagnie.

 

from gamegrid import *
from java.awt import Point

# ---------------- class Animal ----------------
class Animal():
    def __init__(self, imgPath): 
        self.imagePath = imgPath 
    def showMe(self, x, y): 
         bg.drawImage(self.imagePath, x, y)

# ---------------- class Pet ----------------
class Pet(Animal):   # Derived from Animal
    def __init__(self, imgPath, name):  
        self.imagePath = imgPath 
        self.name = name
    def tell(self, x, y): # Additional method
        bg.drawText(self.name, Point(x, y))

makeGameGrid(600, 600, 1, False)
setBgColor(Color.green)
show()
doRun()
bg = getBg()
bg.setPaintColor(Color.black)

for i in range(5):
    myPet = Pet("sprites/pet.gif", "Trixi")
    myPet.showMe(50 + 100 * i, 100) 
    myPet.tell(72 + 100 * i, 145)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Comme le montre le programme précédent, il est possible d’appeler la méthode myPet.showMe() bien que celle-ci ne soit pas définie dans la classe Pet. Cela vient du fait qu’un animal de compagnie est un (is-a) cas particulier d’animal. La relation entretenue entre les classes Pet et Animal est de ce fait appelée une relation est-un (is-a en anglais).

Puisquela variable d’instance imagePath est définie par le constructeur de la classe Animal, il est possible de remplacer la ligne self.imagePath = imgPath dans le constructeur de Pet par Animal.__init__(self, imagePath) pour déléguer l’initialisation au constructeur de la classe de base Animal.

Pour définir des classes dérivées en Python, on spécifie le nom de la classe de base entre parenthèses juste après le nom de la classe définie. Une classe dérivée peut également hériter des méthodes et attributs de plusieurs classes de base qui seront alors mentionnées entre parenthèses et séparées par des virgules (héritage multiple).

 

 

HIÉRARCHIE DE CLASSES, REDÉFINITION DE MÉTHODES

 
Les méthodes définies dans la classe de base peuvent être redéfinies dans la classe dérivée. Il suffit d’y définir une méthode portant le même nom et avec les mêmes paramètres que dans la classe de base. Pour modéliser des chiens qui aboient avec la méthode tell(), on crée une classe Dog dérivée de Pet et on redéfinit la méthode tell() avec un comportement propre à la classe Dog de sorte qu’elle affiche « Waoh » (= Wouf en anglais). On peut ensuite aussi définir des chats par une classe Cat et leur dire de miauler en redéfinissant la méthode tell() de sorte qu’elle affiche « Meow » (=Miaou pour les chats anglais …).  
 


Les quatre classes ainsi définies et permettant de représenter tous ces animaux peuvent être représentées dans un diagramme de classes. Celui-ci exhibe particulièrement bien la relation “est-un” entre les classes. [plus...Il existe des outils permettant de générer automatiquement, à partir d’un diagramme de classes,
un squelette de code servant à définir ces classes. La nomenclature la plus largement utilisée pour
représenter les diagrammes de classes est l’UML (Unified Modeling Language)
].

Chaque classe est représentée par une boîte rectangulaire dans laquelle figure le nom de la classe tout en haut. Après une barre horizontale de séparation, on trouve juste sous le nom de la classe les variables d’instance. Ensuite viennent le constructeur suivi des méthodes de la classe. La hiérarchie des classes ainsi formée est facilement repérable si l’on arrange les boîtes et les flèches intelligemment.

from gamegrid import *

# ---------------- class Animal ----------------
class Animal():
    def __init__(self, imgPath): 
        self.imagePath = imgPath 
    def showMe(self, x, y):  
         bg.drawImage(self.imagePath, x, y) 
         
# ---------------- class Pet ----------------
class Pet(Animal): 
    def __init__(self, imgPath, name): 
        Animal.__init__(self, imgPath) 
        self.name = name
    def tell(self, x, y):
        bg.drawText(self.name, Point(x, y))

# ---------------- class Dog ----------------
class Dog(Pet):
    def __init__(self, imgPath, name): 
        Pet.__init__(self, imgPath, name)       
    def tell(self, x, y): # Overriding
        bg.setPaintColor(Color.blue)
        bg.drawText(self.name + " tells 'Waoh'", Point(x, y))

# ---------------- class Cat ----------------
class Cat(Pet):
    def __init__(self, imgPath, name):
        Pet.__init__(self, imgPath, name) 
    def tell(self, x, y): # Overriding
        bg.setPaintColor(Color.gray)
        bg.drawText(self.name + "  tells 'Meow'", Point(x, y))

makeGameGrid(600, 600, 1, False)
setBgColor(Color.green)
show()
doRun()
bg = getBg()

alex = Dog("sprites/dog.gif", "Alex")
alex.showMe(100, 100) 
alex.tell(200, 130)  # Overriden method is called

rex = Dog("sprites/dog.gif", "Rex")
rex.showMe(100, 300) 
rex.tell(200, 330)  # Overriden method is called

xara = Cat("sprites/cat.gif", "Xara")
xara.showMe(100, 500) 
xara.tell(200, 530)  # Overriden method is called
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

On peut modifier le comportement de la classe de base en la dérivant et en redéfinissant ses méthodes dans la classe dérivée. Lors de l’invocation de méthodes de la même classe ou d’une classe de base, il ne faut pas oublier le self qui doit impérativement préfixer leur nom. Attention cependant à ne pas insérer le self en tant que premier argument lors de l’invocation d’une méthode. Python s’en charge à la place du programmeur.

Parfois, il faut pouvoir invoquer depuis la méthode XX() surchargée dans la classe dérivée ChildClass la méthode XX() originale définie dans la classe parente BaseClass. Pour réaliser cela, il faut utiliser la syntaxe BaseClass.XX(self, …) en prenant bien soin de transmettre ensuite les paramètres nécessaires [plus... Un oubli de cette règle va conduire la fonction surchargée à s’appeler elle-même de manière récursive, ce qui conduira le programme au plantage].

Cette règle s’applique très souvent au constructeur __init__() de la classe dérivée si l’on veut invoquer le constructeur de la classe de base depuis le constructeur de la classe dérivée. Au sein de la classe dérivée, l’appel au constructeur de la classe de base ressemblera donc à

class BaseClass:

    def __init__(self, a):
        self.a = a

class ChildClass(BaseClass):

    def __init__(self, a, b):
        # on délègue au constructeur de la classe de base l'initialisation de
        # la variable d'instance `a`
        BaseClass.__init__(self, a)
        self.b = b

 

 

POLYMORPHISME: APPEL DE MÉTHODES DIRIGÉE PAR LE TYPE

 

Le polymorphisme est un peu plus ardu à comprendre mais il constitue une fonctionnalité essentielle de la programmation orientée objets. Il concerne l’appel des fonctions redéfinies (overridden methods) dans les classes dérivées en adaptant l’appel en fonction de la classe à laquelle l’instance est affiliée. Voici un exemple qui permettra de clarifier cette notion : un programme définit une liste Animals contenant des instances d’animaux de compagnie de types différents  

animals = [Dog(), Dog(), Cat()]

Le fait de parcourir cette liste et d’invoquer la méthode tell() poserait problème sans le polymorphisme car il y a trois méthodes différentes nommées tell() parmi les classes Pet, Dog et Cat

for animal in animals:
    animal.tell()

L’interpréteur Python pourrait résoudre cette ambiguïté de trois manières différentes :

  1. En déclenchant une erreur
  2. En appelant la méthode tell() de la classe de base Pet
  3. En invoquant la version de tell() appropriée, selon le type de chaque animal

Dans un langage polymorphique tel que Python, c’est la troisième solution, de loin la meilleure, qui est utilisée.

from gamegrid import *
from soundsystem import *

# ---------------- class Animal ----------------
class Animal():
    def __init__(self, imgPath): 
        self.imagePath = imgPath 
    def showMe(self, x, y):  
         bg.drawImage(self.imagePath, x, y) 
         
# ---------------- class Pet ----------------
class Pet(Animal): 
    def __init__(self, imgPath, name): 
        Animal.__init__(self, imgPath) 
        self.name = name
    def tell(self, x, y):
        bg.drawText(self.name, Point(x, y))

# ---------------- class Dog ----------------
class Dog(Pet):
    def __init__(self, imgPath, name): 
         Pet.__init__(self, imgPath, name)  
    def tell(self, x, y): # Overridden
         Pet.tell(self, x, y)
         openSoundPlayer("wav/dog.wav")
         play()

# ---------------- class Cat ----------------
class Cat(Pet):
    def __init__(self, imgPath, name):
        Pet.__init__(self, imgPath, name) 
    def tell(self, x, y): # Overridden
        Pet.tell(self, x, y)
        openSoundPlayer("wav/cat.wav")
        play()

makeGameGrid(600, 600, 1, False)
setBgColor(Color.green)
show()
doRun()
bg = getBg()

animals = [Dog("sprites/dog.gif", "Alex"), 
     Dog("sprites/dog.gif", "Rex"), 
     Cat("sprites/cat.gif", "Xara")]

y = 100
for animal in animals:
    animal.showMe(100, y)     
    animal.tell(200, y + 30)    # Which tell()???? 
    show()
    y = y + 200
    delay(1000)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Le polymorphisme fait en sorte que ce soit l’affiliation à la classe qui détermine laquelle des méthodes sera invoquée dans le cas de méthodes redéfinies dans les classes filles. Du fait qu’en Python l’appartenance à une certaine classe n’est déterminée qu’au moment de l’exécution, le polymorphisme est trivial.

Le typage dynamique utilisé en Python est appelée duck test ou duck typing selon la citation attribuée à James Whitcomb Riley (1849 – 1916) :

« When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck. »

Lorsque je vois un oiseau (classe de base Bird) qui marche comme un canard, nage comme un canard et cancane comme un canard (méthodes redéfinies dans la classe spécialisée Duck), je considère cet oiseau comme étant un canard (Classe dérivée Duck).

Il arrive qu’une méthode redéfinie soit définie dans la classe de base et que son corps soit vide. Pour cela, on peut se limiter à y mettre une instruction return (qui ne fait donc que de retourner None) ou l’expression vide pass.

 

 

EXERCICES

 

1.


Définir une classe TurtleKid dérivée de la classe Turtle dont la méthode shape() dessine un carré. Une fois cette classe dérivée définie, le code suivant devrait fonctionner sans erreur:

tf = TurtleFrame()
# john is a Turtle
john = Turtle(tf) 
# john knows all commands of Turtle
john.setColor("green") 
john.forward(100)
john.right(90)
john.forward(100)

# laura is a TurtleKid, but also a Turtle
# laura knows all commands of Turtle
laura = TurtleKid(tf) 
laura.setColor("red")
laura.left(45)
laura.forward(100)
# laura knows a new command too
laura.shape()

2.

Définir deux classes dérivées de Turtle, à savoir TurtleBoy et TurtleGirl qui redéfinissent la méthode shape() de sorte qu’une instance TurtleBoy dessine un triangle plein et qu’une instance TurtleGirl dessine un disque plein. Le programme suivant devrait alors pouvoir s’exécuter sans problème:

tf = TurtleFrame()

aGirl = TurtleGirl(tf)
aGirl.setColor("red")
aGirl.left(45)
aGirl.forward(100)
aGirl.shape()

aBoy = TurtleBoy(tf)
aBoy.setColor("green")
aBoy.right(45)
aBoy.forward(100)
aBoy.shape()

aKid = TurtleKid(tf)
aKid.back(100)
aKid.left(45)
aKid.shape()

3.

Dessiner le diagramme de classes de l’exercice 2.

 

   

MATÉRIEL SUPPLÉMENTAIRE


 

VARIABLES ET MÉTHODES STATIQUES

 

Les classes peuvent également être utilisées pour regrouper des variables et fonctions qui ont un but commun dans le but de rendre le code plus lisible. On pourrait par exemple regrouper les principales constantes physiques dans la classe Physics. Des variables définies directement sous l’en-tête de la classe sont appelées variables statiques et on s’y réfère en préfixant leur nom de celui de la classe dont elles font partie. Au contraire des variables d’instance, il n’est pas nécessaire de créer une instance de la classe pour y accéder.

import math

# ---------------- class Physics ----------------
class Physics():
    # Avagadro constant [mol-1]
    N_AVAGADRO = 6.0221419947e23
    # Boltzmann constant [J K-1]
    K_BOLTZMANN = 1.380650324e-23
    # Planck constant [J s]
    H_PLANCK = 6.6260687652e-34;
    # Speed of light in vacuo [m s-1]
    C_LIGHT = 2.99792458e8
    # Molar gas constant [K-1 mol-1]
    R_GAS = 8.31447215
    # Faraday constant [C mol-1]
    F_FARADAY = 9.6485341539e4;
    # Absolute zero [Celsius]
    T_ABS = -273.15
    # Charge on the electron [C]
    Q_ELECTRON = -1.60217646263e-19
    # Electrical permittivity of free space [F m-1]
    EPSILON_0 = 8.854187817e-12
    # Magnetic permeability of free space [ 4p10-7 H m-1 (N A-2)]
    MU_0 = math.pi*4.0e-7

c = 1 / math.sqrt(Physics.EPSILON_0 * Physics.MU_0)
print("Speed of light (calulated): %s m/s" %c)
print("Speed of light (table): %s  m/s" %Physics.C_LIGHT)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

On peut également regrouper des fonctions apparentées en les déclarant comme statiques dans une même classe. On peut ensuite utiliser ces méthodes en préfixant leur nom par celui de leur classe sans avoir à créer une instance de cette classe .

Pour déclarer une méthode comme statique, il faut écrire la ligne @staticmethod juste au-dessus de sa définition.

# ---------------- class OhmsLaw ----------------
class OhmsLaw():
    @staticmethod
    def U(R, I):
        return R * I

    @staticmethod
    def I(U, R):
        return U / R
    
    @staticmethod
    def R(U, I):
        return U / I

r = 10
i = 1.5

u = OhmsLaw.U(r, i)
print("Voltage = %s V" %u)
Sélectionner le code (Ctrl+C pour copier, Ctrl+V pour coller)

 

 

MEMENTO

 

Les variables statiques (également appelées variables de classe) appartiennent à la classe elle-même alors que les variables d’instance sont rattachées à une instance particulière de la classe. Ainsi, toutes les instances d’une même classe partagent les variables de classe entre elles alors que chacune dispose de ses propres copies des variables d’instance. On peut lire et modifier une variable de classe en préfixant son nom de celui de sa classe suivi d’un point.

On utilise typiquement une variable de classe pour implémenter un compteur d’instances générées à partir d’une classe données. On crée alors une variable de classe counter et on incrémente cette variable de classe dans le constructeur __init__ à la création de chaque instance.

Des fonctions apparentées peuvent être regroupées en tant que méthodes de classe (statiques) dans une classe au nom bien choisi. La ligne @staticmethod est un décorateur de fonctions et doit figurer juste au-dessus de la fonction ou méthode décorée.