Infoforall

Python 24 : Les objets - Attributs, Héritage et Polymorphisme

Nous allons voir trois grands principes de la Programmation Orientée Objet. A savoir :

Convention de synthaxe

Vous avez dû remarquer que ça devenait difficile de gérer les fonctions, les méthodes, les classes, les objets ... On s'y perd un peu.

C'est pour cela que la plupart des developpeurs Python adoptent un système de reconnaissance par rapport aux noms. Nous tenterons désormais de respecter la convention volontaire suivante :

Si le nom commence par une majuscule, c'est une Classe.

Si la classe nécessite plusieurs mots, on commence chaque mot par une majuscule.

Ainsi

  • Personnage est une classe.
  • PersonnageNonJoueur est une classe.

Si l'entité commence par une minuscule, ça peut être une variable, une fonction, une méthode ou un objet.

Si le nom n'est composé que de un mot, seul la présence des parenthèses permet de dire que c'est une méthode ou une fonction.

Si le nom comporte plusieurs mots, c'est plus facile :

Pour les variables (que ce soient des 'variables-types' ou des 'variables-classes'), on utilise les majuscules comme éléments séparateurs.

  • personnageNumero1 est normalement une variable.
  • argentPocheGauche est normalement une variable.

Pour les fonctions et les méthodes, on utilise les underscores comme éléments séparateurs.

  • personnage_numero1 est normalement une méthode ou une fonction.
  • argent_poche_gauche est normalement une méthode ou une fonction.

Pour les constantes (variables définient et non modifiées ensuite), on utilise uniquement des MAJUSCULES et des UNDERSCORES.

  • HAUTEUR_IMAGE est normalement une constante.
  • NIVEAU_MAX_PERSONNAGE est normalement une constante.

1 - Variables de classes et variables d'instance

Nous allons voir qu'on peut stocker des choses de deux façons dans un objet :

Prenons le cas d'une classe Voiture ayant un attribut chaineAutoRadio.

Si chaineAutoRadio était un attribut d'instance : changer de chaîne dans votre voiture ne provoque de changement que dans votre voiture (comme dans la vraie vie).

Si chaineAutoRadio était un attribut de classe : changer de chaîne dans votre voiture provoquerait le même changement dans toutes les voitures.

Pour l'instant, nous n'avons créé que des variables d'instance : des variables propres à l'objet en lui-même. Comment ? Tout simplement en les initialisant dans la méthode spéciale __init__, la méthode qui est automatiquement appelée lorsqu'on crée un nouvel objet.

Pour changer un peu des classes version Star Wars (mais surtout version Console !), j'ai écrit une nouvelle version de notre fameux jeu dérivé de Pacman. Cette fois, j'ai créé une classe Pacman qui va créé l'item du pacman lors de l'appel de la méthode spéciale __init__. En voici un extrait :

#!/usr/bin/env python

# -*- coding: utf-8 -*-


from tkinter import *

import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Pacman :

    "Cette classe génère un pacman dans le canvas"

    def __init__(self,leCanvas,x0,y0,largeur,couleur) :

        """

        Le paramètre leCanvas doit être la référence du Canvas dans lequel on veut créer le pacman

        Les paramètres x0 et y0 attendent les integers corresponant aux coordonnées du point Haut Gauche

        Le paramètre largeur attend un integer qui correspont à la largeur de votre pacman

        Le paramètre couleur attend un String contenant la couleur voulue

        """

        self.canvas = leCanvas

        self.fenetre = leCanvas.master

        self.item = leCanvas.create_arc(x0,y0,x0+largeur,y0+largeur,fill=couleur,start=15,extent=330)

        self.vitMax = 10

        self.vitX = 0

        self.vitY = 0

        self.modAngle = 0

        self.activer_animation()


span class="qu">01° On voit que la classe ne possède pas de paramètre lors de sa déclaration. Par contre, nouveauté par rapport aux classes précédentes : la méthode spéciale __init__ possède des paramètres ! Quels sont les paramètres nécéssaires à la méthode __init__ ?

Nous avions créé les classes (dont le code est donné ci-dessous) dans le fichier mes_classes.py.

#!/usr/bin/env python

# -*- coding: utf-8 -*-


from tkinter import *

import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Pacman :

    "Cette classe génère un pacman dans le canvas"

    def __init__(self,leCanvas,x0,y0,largeur,couleur) :

        """

        Le paramètre leCanvas doit être la référence du Canvas dans lequel on veut créer le pacman

        Les paramètres x0 et y0 attendent les integers corresponant aux coordonnées du point Haut Gauche

        Le paramètre largeur attend un integer qui correspont à la largeur de votre pacman

        Le paramètre couleur attend un String contenant la couleur voulue

        """

        self.canvas = leCanvas

        self.fenetre = leCanvas.master

        self.item = leCanvas.create_arc(x0,y0,x0+largeur,y0+largeur,fill=couleur,start=15,extent=330)

        self.vitMax = 10

        self.vitX = 0

        self.vitY = 0

        self.modAngle = 0

        self.activer_animation()


def activer_animation(self): laFenetre = self.fenetre leCanvas = self.canvas lePacman = self.item direction = random.randint(1,100) if direction > 80: if direction > 95: self.vitX = self.vitMax self.vitY = 0 elif direction > 90: self.vitX = -self.vitMax self.vitY = 0 elif direction > 85: self.vitX = 0 self.vitY = self.vitMax else: self.vitX= 0 self.vitY = -self.vitMax self.modAngle = 0 liste_coord = leCanvas.coords(lePacman) # liste_coord contient les coordonnées du point supérieur gauche xA,yA et inférieur droit xB, yB # Ordre des indices : 0 pour xA, 1 pour yA, 2 pour xB et 3 pour yB if liste_coord[2]>500 : self.vitX = -self.vitMax elif liste_coord[0]<0 : self.vitX = self.vitMax if liste_coord[1]<0 : self.vitY = 10 elif liste_coord[3]>600 : self.vitY = -self.vitMax if ((self.vitX <0) or (self.vitY>0)) : self.modAngle = 180 if self.vitY == 0: if (leCanvas.itemcget(lePacman, 'start') == '15.0' or leCanvas.itemcget(lePacman, 'start') == '195.0'): leCanvas.itemconfig(lePacman, start=30+self.modAngle, extent=300) else: leCanvas.itemconfig(lePacman, start=15+self.modAngle, extent=330) if self.vitX ==0 : if (leCanvas.itemcget(lePacman, 'start') == '105.0' or leCanvas.itemcget(lePacman, 'start') == '285.0'): leCanvas.itemconfig(lePacman, start=120+self.modAngle, extent=300) else: leCanvas.itemconfig(lePacman, start=105+self.modAngle, extent=330) leCanvas.move(lePacman,self.vitX,self.vitY) laFenetre.after(100, self.activer_animation) # ---------------------------------------------------------------- # Fonctions # ---------------------------------------------------------------- def rien(): nothing() # ---------------------------------------------------------------- # Corps du programme # ---------------------------------------------------------------- fen_princ = Tk() fen_princ.title("CLASSE PACMAN") fen_princ.geometry("600x600") monCanvas = Canvas(fen_princ, width=500, height=600, bg='ivory', bd=0, highlightthickness=0) monCanvas.grid(row=0,column=0, padx=10,pady=10) pacman_1 = Pacman(monCanvas,50,50,50,"yellow") pacman_2 = Pacman(monCanvas,200,200,50,"yellow") pacman_3 = Pacman(monCanvas,400,100,50,"yellow") zone2 = Frame(fen_princ, bg='#777777') zone2.grid(row=0,column=1,ipadx=5) but01 = Button(zone2, text="Bouton A", fg="yellow", bg="black", command=rien) but02 = Button(zone2, text="Bouton B", fg="yellow", bg="black", command=rien) but03 = Button(zone2, text="Bouton C", fg="yellow", bg="black", command=rien) but04 = Button(zone2, text="Bouton D", fg="yellow", bg="black", command=rien) but05 = Button(zone2, text="Bouton E", fg="yellow", bg="red", command=rien) but06 = Button(zone2, text="Bouton F", fg="yellow", bg="red", command=rien) but01.pack(fill=X) but02.pack(fill=X) but03.pack(fill=X) but04.pack(fill=X) but05.pack(fill=X) but06.pack(fill=X) fen_princ.mainloop()

#!/usr/bin/env python

# -*- coding: utf-8 -*-


import random


# - - - - - - - - - - - - - - - - - -

# Déclarations des classes

# - - - - - - - - - - - - - - - - - -


class Arme:

    "Cette classe contient les infos sur une arme"

    def __init__(self):

        self.nom='poings'

        self.degats=1

        self.type='[arme]'

        self.distance='contact'

    def presentation(self):

        "Affiche quelques unes des valeurs de l'arme"

        print('Arme : ', self.nom, ' qui fait ',self.degats, 'dégats à la distance :',self.distance)


class Armure:

    "Cette classe contient les infos sur une armure"

    def __init__(self):

        self.nom='aucune'

        self.protection=0

        self.type='[armure]'

    def presentation(self):

        "Affiche quelques unes des valeurs stockées de l'armure"

        print('Armure :', self.nom, ' qui protège de ',self.protection, 'dégats')


class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"

    def __init__(self):

        self.nom='nobody'

        self.prenom='nobody'

        self.niveau=0

        self.classe='aucune classe'

        self.arme = Arme()

        self.armure = Armure()


    def presentation(self):

        "Affiche quelques unes des valeurs stockées dans le personnage"

        print(self.nom, ' ',self.prenom)

        print(self.classe, ' de niveau ',self.niveau)

        self.arme.presentation()

        self.armure.presentation()


    def perdre_un_niveau(self):

        "Cette méthode permet de réduire le niveau de 1"

        self.niveau +=-1


    def combat(self,adversaire):

        "Ceci est une méthode qui renvoie le nom du combattant qui perd"

        perdant =''

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.niveau

            valeur2 = random.randint(1,6)+adversaire.niveau

            if valeur1>valeur2:

                perdant = adversaire.nom

                adversaire.perdre_un_niveau()

                print(adversaire.nom, "vient de perdre un combat. Il lui reste ",adversaire.niveau, " niveaux.")

                test = False

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                print(self.nom, "vient de perdre un combat. Il lui reste ",self.niveau, " niveaux.")

                test = False

        return(perdant)

01° Modifier ou créer votre fichier mes_classes.py.

Pour gagner un peu de temps pendant les phases de tests, nous aimerions pouvoir tester les fonctionnalités de nos classes sans avoir à passer par un fichier supplémentaire qui contiendrait notre code de test.

Pour faire cela, nous allons insérer du code à la fin du fichier mes_classes.py. Ce code ne s'exécutera que si on lance le fichier directement en cliquant dessus. Par contre, si un autre code fait appel à ce fichier (via import mes_classes), il ne se passera rien.

02° Rajouter les lignes de code suivantes sous le return de la classe Personnage dans le fichier mes_classes.py. Ne placer aucune indentation. Lancer et vérifier que le code s'exécute bien.

#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")

    print("Le contenu de __name__ est :")

    print(__name__,'\n')

    input("Appuyer sur ENTREE")

Vous devriez voir ceci s'afficher dans la console :

** Code de test du fichier mes_classes.py **

Le contenu de __name__ est :

__main__


Appuyer sur ENTREE

Sans rentrer dans les détails, la variable __name__ contient bien "__main__" uniquement si l'exécution du code est provoqué par l'action directe de l'utilisateur. Ainsi, si Python exécute ce fichier parce que vous avez cliqué sur le fichier, il va rentrer dans votre test IF.

Par contre, si Python exécute mes_classes.py suite à la lecture d'un import mes_classes dans un autre programme, la variable __main__ ne contiendra pas "__main__" car mes_classes.py n'est pas le programme principal mais un programme secondaire.

Bien. Il est temps de rentrer dans le coeur de cette partie : attributs d'instance ou de Classe ?

03° Remplacer le test de code par le test suivant. On crée deux instances perso1 et perso2 de Personnage. On modifie ensuite le nom de perso1 et on vérifie que cela ne modifie pas le nom de perso2 qui est une autre instance.

#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")

    perso1=Personnage()

    perso2=Personnage()

    perso1.nom="Bob"

    print("\n** Test de perso1.nom **")

    print(perso1.nom)

    print("\n** Test de perso2.nom **")

    print(perso2.nom)

    input("\nAppuyer sur ENTREE")

Vous devriez constater que le nom "Bob" a été affecté à perso1.nom mais pas à perso2.nom.

** Code de test du fichier mes_classes.py **


** Test de perso1.nom **

Bob


** Test de perso2.nom **

nobody


Appuyer sur ENTREE

Nous allons maintenant créer un attribut de Classe : nous allons définir cette attribut directement dans la Classe, hors de toute méthode.

Nous pourrions par exemple vouloir choisir un niveau de départ via un attribut niveauDepart qu'on placerait initialement à 3.

class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"

    niveauDepart = 3

    def __init__(self):

        self.nom='nobody'

        self.prenom='nobody'

        self.niveau=0

        self.classe='aucune classe'

        self.arme = Arme()

        self.armure = Armure()


    def presentation(self):

        "Affiche quelques unes des valeurs stockées dans le personnage"

        print(self.nom, ' ',self.prenom)

        print(self.classe, ' de niveau ',self.niveau)

        self.arme.presentation()

        self.armure.presentation()


    def perdre_un_niveau(self):

        "Cette méthode permet de réduire le niveau de 1"

        self.niveau +=-1


    def combat(self,adversaire):

        "Ceci est une méthode qui renvoie le nom du combattant qui perd"

        perdant =''

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.niveau

            valeur2 = random.randint(1,6)+adversaire.niveau

            if valeur1>valeur2:

                perdant = adversaire.nom

                adversaire.perdre_un_niveau()

                print(adversaire.nom, "vient de perdre un combat. Il lui reste ",adversaire.niveau, " niveaux.")

                test = False

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                print(self.nom, "vient de perdre un combat. Il lui reste ",self.niveau, " niveaux.")

                test = False

        return(perdant)

04° Remplacer le test de code par le test suivant. On crée deux instances perso1 et perso2 de Personnage. On affiche le contenu et l'id de perso1.niveauDepart, idem pour perso2.niveauDepart. On modifie ensuite perso1.niveauDepart et on vérifie que cela ne modifie pas perso2.niveauDepart qui est une autre instance.

#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")


    perso1=Personnage()

    perso2=Personnage()


    print("\n** Test sur perso1.niveauDepart sans modification **")

    print(perso1.niveauDepart)

    print(id(perso1.niveauDepart))


    print("\n** Test sur perso2.niveauDepart sans modification **")

    print(perso2.niveauDepart)

    print(id(perso2.niveauDepart))


    perso1.niveauDepart = 10


    print("\n** Test sur perso1.niveauDepart après modification **")

    print(perso1.niveauDepart)

    print(id(perso1.niveauDepart))


    print("\n** Test sur perso2.niveauDepart après modification **")

    print(perso2.niveauDepart)

    print(id(perso2.niveauDepart))


    input("\nAppuyer sur ENTREE")

Et on obtient :

** Code de test du fichier mes_classes.py **


** Test sur perso1.niveauDepart sans modification **

3

1525658752


** Test sur perso2.niveauDepart sans modification **

3

1525658752


** Test sur perso1.niveauDepart après modification **

10

1525658976


** Test sur perso2.niveauDepart après modification **

3

1525658752


Appuyer sur ENTREE

Conclusion : la modification de perso1.niveauDepart ne modifie pas non plus perso2.niveauDepart. En effet, initialement les deux attributs ont le même id mais lorsqu'on modifie le premier, l'id change : on ne pointe plus vers la même zone mémoire. Il s'agit donc de deux variables indépendantes.

Il est temps d'arriver au coeur de la partie : comme modifier niveauDepart pour qu'il soit modifié dans toutes les instances ?

Il faut en réalité modifier le contenu directement dans la classe avec Personnage.niveauDepart = 10.

05° Remplacer le test de code par le test suivant. On crée deux instances perso1 et perso2 de Personnage. On affiche le contenu et l'id de perso1.niveauDepart, de perso2.niveauDepart mais également de Personnage.niveauDepart. On modifie ensuite Personnage.niveauDepart et on vérifie que cela modifie perso1.niveauDepart et perso2.niveauDepart.

#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")

    perso1=Personnage()

    perso2=Personnage()


    print("\n** Test sur perso1.niveauDepart sans modification **")

    print(perso1.niveauDepart)

    print(id(perso1.niveauDepart))


    print("\n** Test sur perso2.niveauDepart sans modification **")

    print(perso2.niveauDepart)

    print(id(perso2.niveauDepart))


    print("\n** Test sur Classe.niveauDepart sans modification **")

    print(Personnage.niveauDepart)

    print(id(Personnage.niveauDepart))


    Personnage.niveauDepart = 10


    print("\n** Test sur perso1.niveauDepart après modification **")

    print(perso1.niveauDepart)

    print(id(perso1.niveauDepart))


    print("\n** Test sur perso2.niveauDepart après modification **")

    print(perso2.niveauDepart)

    print(id(perso2.niveauDepart))


    print("\n** Test sur Classe.niveauDepart après modification **")

    print(Personnage.niveauDepart)

    print(id(Personnage.niveauDepart))


    input("\nAppuyer sur ENTREE")

Et on obtient :

** Code de test du fichier mes_classes.py **


** Test sur perso1.niveauDepart sans modification **

3

1525658752


** Test sur perso2.niveauDepart sans modification **

3

1525658752


** Test sur Classe.niveauDepart sans modification **

3

1525658752


** Test sur perso1.niveauDepart après modification **

10

1525658976


** Test sur perso2.niveauDepart après modification **

10

1525658976


** Test sur Classe.niveauDepart après modification **

10

1525658976


Appuyer sur ENTREE

CONCLUSION

Cette fois, ça fonctionne : le fait d'avoir modifier directement un attribut de classe, ici la valeur de Personnage.niveauDepart, permet aux instances de mettre à jour cette valeur également tant qu'elles pointent vers le bon id.

Quand pointent-elles vers le même id que Personnage.niveauDepart ? Tant que vous ne les modifiez pas directement. Lire perso1.niveauDepart ne pose aucun problème.

Quand ne pointent-elles plus vers le même id que Personnage.niveauDepart ? Dès que vous voulez agir directement sur perso1.niveauDepart. A ce moment, elle devient un attribut d'instance.

explication visuelle des attributs de classe ou d'instance

2 - Héritage

Premier pilier de la programmation orientée objet : l'héritage de classe.

Pour l'instant, nous avons créer des classes de base. C'est à dire en la définissant depuis la base.

Mais on peut faire mieux : on peut baser une classe sur une autre classe. La nouvelle classe va alors hériter des méthodes et attributs de l'ancienne classe sans qu'on ai besoin de tout recopier.

Passons à la pratique. Nous allons créer des classes de Personnage légérement différentes en fonction du rôle que le personnage va assumer dans le jeu (on parle d'ailleurs également de classes dans les jeux de rôles issus des mécanismes de Dungeons & Dragons, première version en 1974.)

La classe Personnage contiendra donc tous les éléments basiques : ceux qui seront possédés par tous les personnages.

Ensuite, on rajoutera des éléments en fonction des spécificités de chaque rôle/classe :

Pour rappel, voici les classes du fichier mes_classes.py.

Une seule différence majeure : j'ai rajouté la classe nommée Jedi en bas.

J'ai également modifié l'initialisation de self.niveau pour qu'il soit égal à Personnage.niveauDepart lors de l'instanciation.

class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"

    niveauDepart = 3

    def __init__(self):

        self.nom='nobody'

        self.prenom='nobody'

        self.niveau=Personnage. niveauDepart

        self.classe='aucune classe'

        self.arme = Arme()

        self.armure = Armure()


    def presentation(self):

        "Affiche quelques unes des valeurs stockées dans le personnage"

        print(self.nom, ' ',self.prenom)

        print(self.classe, ' de niveau ',self.niveau)

        self.arme.presentation()

        self.armure.presentation()


    def perdre_un_niveau(self):

        "Cette méthode permet de réduire le niveau de 1"

        self.niveau +=-1


    def combat(self,adversaire):

        "Ceci est une méthode qui renvoie le nom du combattant qui perd"

        perdant =''

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.niveau

            valeur2 = random.randint(1,6)+adversaire.niveau

            if valeur1>valeur2:

                perdant = adversaire.nom

                adversaire.perdre_un_niveau()

                print(adversaire.nom, "vient de perdre un combat. Il lui reste ",adversaire.niveau, " niveaux.")

                test = False

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                print(self.nom, "vient de perdre un combat. Il lui reste ",self.niveau, " niveaux.")

                test = False

        return(perdant)


class Jedi(Personnage):

    "Ceci est une classe permettant de créer un personnage Jedi dans mon super RPG"

06° Modifier ou créer votre fichier mes_classes.py.

Vous remarquerez que, lors de la déclaration de la classe Jedi, nous avons noté class Jedi(Personnage) : cela veut dire de créer une nouvelle classe nommée Jedi mais en se basant sur la classe existante Personnage.

Bien. Il est temps de rentrer dans le coeur de cette activité : l'héritage.

07° Remplacer le test de code par le test suivant. Lancer en utilisant une mauvaise définition de la classe Jedi (class Jedi:) puis la bonne définition (class Jedi(Personnage):. Pourquoi a-t-on une erreur avec class Jedi: et pas avec class Jedi(Personnage): ?

#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")

    print("** Test sur un personnage de classe Personnage **")

    perso1=Personnage()

    perso1.presentation()

    print(perso1)

    print("** Test sur un personnage de classe Jedi **")

    perso2=Jedi()

    perso2.presentation()

    print(perso2)

    input("Appuyer sur ENTREE")

Avec une déclaration de classe de type class Jedi:, on obtient une erreur :

** Code de test du fichier mes_classes.py **

** Test sur un personnage de classe Personnage **

nobody nobody

aucune classe de niveau 0

Arme : poings qui fait 1 dégats à la distance : contact

Armure : aucune qui protège de 0 dégats

<__main__.Personnage object at 0x000001D417F01198>


** Test sur un personnage de classe Jedi **

Traceback (most recent call last):

File "C:\...\mes_classes.py", line 75, in <module>

    perso2.presentation()

AttributeError: 'Jedi' object has no attribute 'presentation'

Avec une déclaration de classe de type class Jedi(Personnage):, on obtient :

** Code de test du fichier mes_classes.py **

** Test sur un personnage de classe Personnage **

nobody nobody

aucune classe de niveau 0

Arme : poings qui fait 1 dégats à la distance : contact

Armure : aucune qui protège de 0 dégats

<__main__.Personnage object at 0x000001D417F01198>


** Test sur un personnage de classe Jedi **

nobody nobody

aucune classe de niveau 0

Arme : poings qui fait 1 dégats à la distance : contact

Armure : aucune qui protège de 0 dégats

<__main__.Jedi object at 0x000002606EBD1358>

...CORRECTION...

Si on utilise la première déclaration (Jedi tout simplement) : Python crée une nouvelle classe de base. Cette classe ne possède donc pas la méthode presentation(), ni d'ailleurs les attributs nom, prenom ...

    perso2.presentation() provoque donc une erreur.

Si on utilise la deuxième déclaration Jedi(Personnage) : Python crée une classe Jedi en y intégrant la possibilité de se comporter comme Personnage. L'instance perso2 de la classe Jedi possède donc bien de base une méthode presentation et les attributs nécéssaires à son fonctionnement !

Voilà le fondement de l'héritage de classe : lorsqu'on crée une classe fille à partir d'une autre classe (la classe mère), on permet à la classe fille d'avoir les mêmes attributs que la classe mère et d'avoir accès aux méthodes de la classe mère.

Cela vous évitera donc de taper en partie 16 fois le même code si vous voulez définir 16 classes de personnages différentes. Et en cas de mise à jour, il suffira de modifier le code de la classe Personnage plutôt que d'aller modifier le code à l'identique à 16 endroits différents. Vous voyez l'intérêt j'espère.

Ainsi, nous n'avons défini aucun attribut et aucune méthode dans la classe Jedi mais Jedi hérite de ceux de sa classe mère Personnage.

explication visuelle de l'héritage

C'est bien beau mais pour l'instant, la classe Jedi et la classe Personnage sont strictement identiques en réalité : on crée Jedi en copiant juste les plans de Personnage.

Nous allons maintenant rajouter des éléments à Jedi, à savoir des attributs et des méthodes. Nous allons créer une méthode manipulation(adversaire) qui permet au Jedi de manipuler mentalement son adversaire pour ne pas avoir à se battre contre lui. Le fameux "Ceci n'est pas l'androïde que vous recherchez". Pour cela, j'ai également rajouté un attribut (de classe) forceMentale. La méthode fonctionne presque comme la méthode combat(adversaire) : on compare 1d6+forceMentale du Jedi à 1d6 de l'adversaire. Si le Jedi gagne, le combat n'a pas lieu et le Jedi pourra continuer son aventure. Si l'adversaire gagne, cela provoque un combat entre le Jedi et l'adversaire.

class Jedi(Personnage):

    "Objet-personnage qui contient tout pour décrire votre personnage adepte de la Force"


    forceMentale = 2


    def manipulation(self, adversaire):

        "Ceci est une méthode qui renvoie le nom de l'adversaire manipulé mentalement"

        perdant =""

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.forceMentale

            valeur2 = random.randint(1,6)

            if valeur1>valeur2:

                perdant = adversaire.nom

                test = False

                print(self.nom," tente de convaincre ",adversaire.nom," qu'il n'est pas nécessaire de se battre. ")

                print(adversaire.nom, " est d'accord et le laisse passer.")

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                test = False

                print(self.nom," tente de convaincre ",adversaire.nom," qu'il n'est pas nécessaire de se battre.")

                print(adversaire.nom, " se rend compte de la mnanipulation et s'énerve.")

                adversaire.combat(self)

        return(perdant)

#########################################################

# Corps du programme

#########################################################

if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")


    perso1=Personnage()

    perso1.nom="Stormtrooper"

    perso1.prenom="Je suis juste un figurant"


    perso2=Jedi()

    perso2.nom="Skywalker"

    perso2.prenom="Bob"

    perso2.manipulation(perso1)


    input("Appuyer sur ENTREE")

08° Utiliser la nouvelle classe et le nouveau programme test, puis répondre aux questions suivantes en rajoutant (si possible) à chaque fois un bout de code permettant de valider ou infirmer votre réponse :

  1. Peut-on taper perso1.manipulation(perso2) ?
  2. De quel type doit-être le paramètre adversaire de la méthode manipulation() ?
  3. De quel type doit-être le paramètre adversaire de la méthode combat() ?
  4. perso2 peut-il utiliser combat() ?
  5. Que vaut forceMentale pour perso2 ? Que vaut forceMentale pour perso1 ?
  6. Si on crée un perso3 de classe Jedi, peut-on utiliser perso2.manipulation(perso3) ?

Quelques réponses et quelques bouts de code pour bien comprendre le principe de l'héritage :

Question 1 :

Peut-on taper perso1.manipulation() ?

Non : perso1 est une instance de Personnage, classe de base qui ne contient pas de méthode manipulation(). Cela se voit dans la description de la Classe Personnage.

Si on rajoute perso1.manipulation(perso2), on obtient :

Traceback (most recent call last):

  File "C:\Users\...\mes_classes.py", line 99, in <module>

    perso1.manipulation(perso2)

AttributeError: 'Personnage' object has no attribute 'manipulation'

Question 2 :

De quel type doit-être le paramètre adversaire de la méthode manipulation() ?

Là, nulle méthode magique n'est utilisable. Il faut analyser le code fourni.

Après analyse, il faut un objet possédant un attribut nom car on voit adversaire.nom dans le code, et une méthode combat() car on voit adversaire.combat(self) apparaître dans le code.

Les classes disposant de cela sont Personnage et Jedi.

Question 3 :

De quel type doit-être le paramètre adversaire de la méthode combat() ?

Idem. Pour voir les dépendances à adversaire, vous pouvez sélectionner adversaire dans votre programme de codage, même Notepad++. Vous allez voir les éléments identiques être mis en valeur.

Bilan : il faut des attributs nom, niveau, et une méthode perdre_un_niveau().

Il s'agit donc des classes Personnage et par héritage les classes filles de Personnage, Jedi pour l'instant.

Les classes disposant de cela sont Personnage et Jedi.

Le paramètre Adversaire doit donc correspondre à une instance d'une de ces deux classes.

Question 4 :

perso2 peut-il utiliser combat() ?

Cela revient à savoir si on peut écrire perso2.combat(adversaire).

L'objet perso2 est une instance de la classe Jedi, classe qui ne possède pas de méthode combat(). Par contre, c'est une classe fille de Personnage. Et Personnage possède une méthode combat().

En conclusion, perso2 peut accéder à la méthode par héritage car elle est la fille de la classe Personnage.

Si on teste perso2.combat(perso1), on obtient un bon fonctionnement. Par exemple :

Stormtrooper vient de perdre un combat. Il lui reste -1 niveaux.

Question 5 :

Que vaut forceMentale pour perso2 ? Que vaut forceMentale pour perso1 ?

L'attribut forceMentale provient de la classe Jedi.

L'objet perso1 ne possède donc pas d'attribut de ce nom.

L'objet perso2 possède un attribut à la valeur 2 comme toutes les instances de cette classe.

On peut tester ces affirmations avec des tests comme print(perso2.forceMentale) et print(perso1.forceMentale).

print(perso2.forceMentale) : donne bien la bonne valeur.

2

print(perso1.forceMentale) donne une erreur comme prévue.

Traceback (most recent call last):

  File "C:\Users\...\mes_classes.py", line 99, in <module>

    print(perso1.forceMentale)

AttributeError: 'Personnage' object has no attribute 'forceMentale'

Question 6 :

Si on crée un perso3 de classe Jedi, peut-on utiliser perso2.manipulation(perso3) ?

En toute logique, oui car la méthode manipulation attend une instance de Personnage ou Jedi.

On peut taper perso2.manipulation(perso3) et cela fonctionne bien.

09° Créer une méthode __init__ dans la classe Jedi pour définir les attributs d'instance puissance (le pouvoir du Jedi est-il puissant ?), finesse (le Jedi peut-il faire des choses subtiles et délicates avec son pouvoir ?) et stabilite (le Jedi est-il sensible au Côté Obscure de la Force ?). On placera les éléments à 10 sur 20 lors de cette initialisation d'instance. Au passage, transformer forceMentale en attribut d'instance et l'initialiser à 2 lors de cette instanciation.

Vous devriez obtenir une méthode __init__ ressemblant à ceci dans la classe Jedi :

class Jedi(Personnage):

    "Objet-personnage qui contient tout pour décrire votre personnage adepte de la Force"


    def __init__(self):

        self.forceMentale = 2

        self.puissance = 10

        self.finesse = 10

        self.stabilite = 10


    # Le reste est inchangé

10° Remplacer le code-test de mes_classes.py par ceci. Tester ensuite le code via IDLE (c'est important pour la suite).

#########################################################

# Corps du programme

#########################################################

if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")


    perso1=Personnage()

    perso1.nom="Stormtrooper"

    perso1.prenom="Je suis juste un figurant"


    perso2=Jedi()

    perso2.nom="Skywalker"

    perso2.prenom="Bob"


    input("Appuyer sur ENTREE")

Tout devrait fonctionner correctement. On vient juste de créer un objet de classe Personnage et lui affecter un nom et un prénom. On fait de même avec un objet de classe Jedi.

11° Rester sur la console/shell de l'IDLE. Tapez les instructions ci-dessous (celles au fond jaune). Vous devriez obtenir la même chose. Auriez-vous une raison à fournir pour expliquer l'erreur finale ?

** Code de test du fichier mes_classes.py **

Appuyer sur ENTREE

>>> perso1.nom

'Stormtrooper'

>>> perso2.nom

'Skywalker'

>>> perso2.niveau

Traceback (most recent call last):

  File "", line 1, in

    perso2.niveau

AttributeError: 'Jedi' object has no attribute 'niveau'

>>>

Correction :

En réalité, il y a très peu de chance que vous trouviez par vous-même sans connaissance préalable. Je voulais surtout vous montrer que parfois, on pense avoir devant soi un code qui marche. Mais non. Car on a oublié un point de détail.

Ici, le point de détail se nomme polymorphisme et c'est l'objet de la partie suivante.

3 - Polymorphisme

Il existe plusieurs formes de polymorphisme. Celle qui nous intéresse aujourd'hui est celle liée aux classes filles et aux classes mères. Pour faire simple, disons que s'il existe plusieurs méthodes portant le même nom, Python va d'abord aller chercher celle qui est la plus proche de l'objet. Nous allons retrouver des choses proches de la portée des variables et de l'espace des noms.

L'erreur de la partie précédente vient de là : il existe maintenant deux méthodes __init__ : une dans Personnage et une dans Jedi.

explication visuelle du polymorphisme

La méthode __init__ que nous venons de créer dans la classe Jedi a écrasé l'appel de la méthode __init__ qui porte le même nom dans la classe Personnage.

Lorsqu'on crée une nouvelle instance de Jedi,Python va chercher une méthode __init__ au plus proche : celle de Jedi. C'est celle où on crée les attributs puissance... Or, c'est dans celle de Personnage qu'on crée les attributs nom, prenom ... !

Pourquoi est-ce que cela fonctionnait avant alors ? Tout simplement car la méthode __init__ n'existait pas dans Jedi. Python allait donc voir dans la classe Personnage.

explication visuelle du cas sans __init__

Attention donc, il va falloir tenir compte des noms des méthodes des classes filles si vous ne voulez pas que des erreurs se produisent. Le problème ici, c'est que la méthode __init__ est une méthode spéciale et que son nom est imposée. Comment faire alors pour que les instances de Jedi puissent utiliser le __init__ de Jedi ET Personnage ?

Et bien le plus simple est encore de lui dire de le faire : dans la méthode __init__ de la classe Jedi, il faudrait donc rajouter l'instruction Personnage.__init__(self) : vous demandez alors à Python d'aller exécuter les instructions de la méthode __init__ qu'on trouve dans Personnage.

12° Rajouter cette ligne de code dans la méthode __init__ de Jedi. Relancer le code via IDLE. Normalement, il ne doit pas y avoir d'erreur, comme la dernière fois.

13° Refaire ensuite les instructions via la console de l'IDLE (ou en les rajoutant dans le programme test, mais il faudra rajouter des "print") :

En relançant le code test, vous devriez obtenir ceci :

** Code de test du fichier mes_classes.py **

Appuyer sur ENTREE

>>> perso1.nom

'Stormtrooper'

>>> perso2.nom

'Skywalker'

>>> perso2.niveau

3

Voilà : plus d'erreur. Cette fois, l'instruction perso2.nveau ne génère plus d'erreur alors que perso2 est une instance de Jedi.

Voilà le code complet de ce que vous devriez avoir :

#!/usr/bin/env python

# -*- coding: utf-8 -*-

import random


#########################################################

# Déclarations des classes

#########################################################


class Arme:

    "Cette classe contient les infos sur une arme"


    def __init__(self):

        self.nom='poings'

        self.degats=1

        self.type='[arme]'

        self.distance='contact'


    def presentation(self):

        "Affiche quelques unes des valeurs de l'arme"

        print('Arme : ', self.nom, ' qui fait ',self.degats, 'dégats à la distance :',self.distance)


class Armure:

    "Cette classe contient les infos sur une armure"


    def __init__(self):

        self.nom='aucune'

        self.protection=0

        self.type='[armure]'


    def presentation(self):

        "Affiche quelques unes des valeurs stockées de l'armure"

        print('Armure :', self.nom, ' qui protège de ',self.protection, 'dégats')


class Personnage:

    "Ceci est une classe permettant de créer un personnage dans mon super RPG"

    niveauDepart = 3


    def __init__(self):

        self.nom='nobody'

        self.prenom='nobody'

        self.niveau=Personnage.niveauDepart

        self.classe='aucune classe'

        self.arme = Arme()

        self.armure = Armure()


    def presentation(self):

        "Affiche quelques unes des valeurs stockées dans le personnage"

        print(self.nom, ' ',self.prenom)

        print(self.classe, ' de niveau ',self.niveau)

        self.arme.presentation()

        self.armure.presentation()


    def perdre_un_niveau(self):

        "Cette méthode permet de réduire le niveau de 1"

        self.niveau +=-1


    def combat(self,adversaire):

        "Ceci est une méthode qui renvoie le nom du combattant qui perd"

        perdant =''

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.niveau

            valeur2 = random.randint(1,6)+adversaire.niveau

            if valeur1>valeur2:

                perdant = adversaire.nom

                adversaire.perdre_un_niveau()

                print(adversaire.nom, "vient de perdre un combat. Il lui reste ",adversaire.niveau, " niveaux.")

                test = False

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                print(self.nom, "vient de perdre un combat. Il lui reste ",self.niveau, " niveaux.")

                test = False

        return(perdant)


class Jedi(Personnage):

    "Objet-personnage qui contient tout pour décrire votre personnage adepte de la Force"


    def __init__(self):

        Personnage.__init__(self)

        self.forceMentale = 2

        self.puissance = 10

        self.finesse = 10

        self.stabilite = 10


    def manipulation(self, adversaire):

        "Ceci est une méthode qui renvoie le nom de l'adversaire manipulé mentalement"

        perdant =""

        test = True

        while test:

            valeur1 = random.randint(1,6)+self.forceMentale

            valeur2 = random.randint(1,6)

            if valeur1>valeur2:

                perdant = adversaire.nom

                test = False

                print(self.nom," tente de convaincre ",adversaire.nom," qu'il n'est pas nécessaire de se battre. ")

                print(adversaire.nom, " est d'accord et le laisse passer.")

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                test = False

                print(self.nom," tente de convaincre ",adversaire.nom," qu'il n'est pas nécessaire de se battre.")

                print(adversaire.nom, " se rend compte de la mnanipulation et s'énerve.")

                adversaire.combat(self)

        return(perdant)


#########################################################

# Corps du programme

#########################################################


if __name__ == "__main__":

    print("** Code de test du fichier mes_classes.py **")


    perso1=Personnage()

    perso1.nom="Stormtrooper"

    perso1.prenom="Je suis juste un figurant"


    perso2=Jedi()

    perso2.nom="Skywalker"

    perso2.prenom="Bob"


    input("Appuyer sur ENTREE")

Si on résumé ce que nous avons vu aujourd'hui :

Attributs de classe ou d'instance : on peut définir des attributs appartenant à la classe (et donc commun à toutes les instances de la classe) ou des attributs d'instance n'appertenant qu'à une instance unique. On peut notamment utiliser la méthode spéciale __init__ pour créer les attributs d'instance.

Héritage : On peut batir une classe sur la structure d'une autre classe. La classe mère hérite alors des méthodes et attributs de classe de la classe mère.

Polymorphisme : Comme pour la portée des noms de 'variables', il existe une portée des noms des méthodes. Python cherche d'abord dans la classe, puis remonte la structure des classes mères... Attention notamment à la méthode __init__ du coup.

Par contre, si on regarde bien, on peut voir qu'on définit que Jedi est une classe fille de Personnage et on est obligé de noter Personnage.__init__(self) dans le __init__ de Jedi. Cela semble un peu redondant.

14° Remplacer Personnage.__init__(self) par super().__init__(). Vous devriez constater que cela marche aussi bien.

L'avantage par contre, c'est qu'on a plus besoin de faire référence une deuxième fois à la classe mère. Si vous voulez changer de référence pour Jedi, vous risquez moins d'erreurs liées à l'oubli de modifications de code.

4 - object

Maintenant que vous avez vu l'héritage et le polymorphisme, nous allons pouvoir expliquer ceci :

Rappel : Python 3 vs Python 2

class Personne(object): ou class Personnage ?

Si vous cherchez des codes dans une documentation, vous trouverez surement des classes définies à l'aide de class Personnage(object):. Il s'agit d'une ancienne façon de déclarer les classes en Python 2. Ce terme object fut rajouté suite à une mise à jour de façon à permettre aux classes de Python 2 de se comporter réellement comme de vraies classes. Comme cette déclaration est rétrocompatible en Python 3, on trouve beaucoup de codes qui incluent cette façon de faire. Mais en Python 3, si vous ne notez rien entre les parenthèses, Python réagira comme si vous aviez rajouté object.

En résumé, en Python 3, que vous notiez def Personnage: ou def Personnage(object): vous obtiendrez la même chose.

Nous expliquerons d'où vient cet object dans cette troisième activité sur les objets.

Cela veut dire que lorsque vous demandez de créer votre 'nouvelle' classe Personnage, elle hérite, gagne, adopte, profite, imite la classe 'de base' : object.

La nouvelle classe fille va ainsi pouvoir utiliser toutes les méthodes et tous les attributs déjà créés dans la classe mère.

Ainsi, même une classe de base, comme Personnage hérite en réalité des méthodes de la 'classe' object.

Et que contient cette structure de base ?

Le mieux, c'est encore d'aller voir à la source, dans la documentation Python. Pour Python 3.6, c'est ici : Python3.6 : Special method names (sur python.org).

Le but ici n'est vraiment pas de faire une présentation réelle de cette documentation. Il s'agit de la fin de l'activité, le but est juste de vous présenter deux trois choses sympathiques, et vous montrer où chercher avant de vous lancer dans les choses compliquées : quelqu'un a déjà certainement prévues une méthode spéciale qui vous permettra de gagner du temps !

Méthode __new__ : cette méthode spéciale CONSTRUCTEUR de object attend un type de classe en argument. Si la création se passe bien, elle invoque la méthode __init__. C'est la première méthode invoquée lorsque vous créez une nouvelle instance d'un objet.

Méthode __init__ : cette méthode spéciale s'exécute suite à son appel via __new__. Nous l'avons déjà bien traitée.

Méthode __del__(self) : cette méthode spéciale DESTRUCTEUR supprime un objet. Si vous voulez supprimer d'autres choses au passage, vous pouvez la modifier mais pensez bien à faire appel également à la méthode de la classe mère via super().__del__(self).

Les méthodes spéciales suivantes permettent d'implanter des réponses aux comparaisons d'objets si vous en avez besoin : perso1 < perso2 par exemple.

Il y en a encore beaucoup mais vous savez où vous informez s'il vous reste encore quelques minutes.

Nous en verrons encore quelques unes dans l'activité suivante sur les objets : l'encapsulation, ou comment tout faire pour qu'un utilisateur ne puisse pas casser les objets qu'il a créé avec votre code.

D'ailleurs, comment informer la personnage qui veut utiliser votre Classe de la bonne façon de le faire ? C'est simple : il faut écrire une documentation claire lors de l'écriture du code. Voyez le résultat un peu bizarre qu'on obtient ici :

15° Si vous êtes encore dans le shell IDLE, tapez >>> help(Personnage) ou >>> help(Jedi).

Comme vous le voyez, c'est pratique. Si c'est bien rempli ...

Nous verrons cela également la fois prochaine.