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.
| .
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.
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()
|
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.
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)
|
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 |
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
|
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.
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)
|
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 |
|
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) 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) |
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. |