Infoforall

Python 25 : Programmation Orientée Objet

L'encapsulation est le fait d'empêcher un utilisateur de manipuler l'objet en dehors des méthodes créées pour cela. Cela revient à mettre l'objet-code dans une boîte noire et à ne permettre d'y toucher depuis l'extérieur qu'à l'aide de méthodes prédéfinies par le créateur de la classe.

Attention : cette activité est plutôt optionnelle, voir carrément hors programme : je suis donc très directif de façon à ne pas perdre trop de temps. C'est néanmoins très intéressant pour ceux qui voudront continuer en informatique.

1 - Encapsulation

Pas de question ici. Simplement la présentation du principe de l'encapsulation.

Jusqu'à présent, nous agissions directement sur les attributs des objets : si on voulait modifier la valeur d'un attribut x d'un objet, nous tapions monObjet.x = 5.

modification directe

L'encapsulation est l'un des principes fondamentaux de la programmation objet. Le principe est simple en soi : vous n'avez pas besoin de connaitre la moindre des lignes de code de l'objet pour le manipuler. Il vous suffit de connaître les méthodes que le créateur de la classe a mis en place.

Cela veut dire que pour modifier la valeur de l'attribut x, vous n'avez pas besoin de connaitre, par exemple les valeurs minimales et maximales autorisées, de créer un test pour s'assurer que si on envoie un mauvais type de variable, cela ne détruira pas votre objet ...

Vous n'avez qu'à faire une modification indirecte : c'est une méthode de la classe (change_x() ici) qui va se charger de modifier la valeur de l'attribut x :

modification indirecte

Il s'agit donc de refuser de modifer ou de lire les valeurs de l'objet en y accédant directement car il y a toujours le risque de placer une mauvaise valeur ou d'oublier qu'en modifiant cette valeur, vous devez également en modifier une autre. Par exemple, si les points de vie x d'un personnage tombe à 0, on doit peut-être placer son état y à "mort"...

Une propriété supplémentaire est parfois introduite dans certains langages : le masquage. Ce principe supplémentaire veut que l'utilisateur ne puisse même pas connaitre l'existence des attributs. Ainsi, il n'a pas le choix : pour modifier ou lire les attributs (x, y et z ici) d'un objet, il ne peut qu'utiliser les méthodes proposées par le créateur de la Classe. En effet, il ne sera même pas que les points de vie sont stockés dans la variable x :

modification indirecte et masquage

La zone bleu foncé est donc totalement cachée de l'utilisateur de la classe. On dit que ces attributs (x,y et z ici) sont privés et plus publiques.

Python est un langage assez libre de ce point de vue : il permet l'encapsulation mais n'impose pas le masquage. Sauf à utiliser un nom particulier pour vos attributs, ceux-ci restent lisibles et modifiables directement. C'est un choix.

Si on prend l'exemple des plans techniques d'une voiture représentant la classe et la voiture réelle représentant l'Objet : vous savez la faire avancer, tourner, allumer la musique mais vous ne connaissez pas absolument pas le fonctionnement interne.

En gros, vous n'avez pas besoin de savoir ce qu'il y a sous le capot pour l'utiliser.

2 - Méthodes Assesseurs

Nous allons continuer à travailler avec nos classes Personnage, Arme et Armure.

Pour rappel, les instances de Personnage possèdent des attributs d'instance nom, prenom, niveau, classe, arme et armure.

On notera que les deux derniers ne sont pas des variables mais des références vers des objets de classe Arme et Armure.

Commençons par les méthodes permettant de lire certains attributs. On les nomme les assesseurs et on leur donne (par convention) un nom qui commence par get.

2.1 Création d'un assesseur

Ainsi, si on veut créer une méthode qui renvoie le nom, on pourrait la nommer get_name pour ne pas donner directement le nom de l'attribut correspondant (nom) et s'ouvrir à l'international en plaçant deux trois mots d'anglais.

Si on crée toutes les méthodes permettant d'accéder aux autres variables internes :

assesseurs

#!/usr/bin/env python

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

import random


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

# Déclarations des classes

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

class Arme(object):

    "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(object):

    "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(object):

    "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 get_name(self):

        "Renvoie un string contenant le nom du personnage"

        return(self.nom)


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

                test = False

            elif valeur2>valeur1:

                perdant = self.nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

Object : j'ai déclaré ici pour l'exemple les classes en notant clairement object. C'est juste pour vous montrer que cela fonctionne aussi. Mais ça ne change rien avec Python 3. C'est juste plus long à taper.

01° Lancer ce code dans IDLE de façon à mettre le code en mémoire. Rester sur le Shell (la console Python de IDLE) et taper les instructions suivantes :

>>> perso1 = Personnage()

>>> perso1.nom

>>> perso1.get_name()

Vous devriez obtenir ceci :

>>> perso1 = Personnage()

>>> perso1.nom

'nobody'

>>> perso1.get_name()

'nobody'

On constate donc bien que les deux façons de faire fonctionnent.

2.2 Convention d'attribut privé : l'underscore simple

En Python, si on veut indiquer à quelqu'un qu'on veut qu'un attribut soit traiter comme un attribut privé, on précéde son nom d'un underscore.

Il s'agit d'une simple convention. Rien n'empêche cette personne de créer un code avec des instructions de type monObjet._x. Mais au moins vous l'avez prévenu. S'il fait n'importe quoi, vous ne serez pas responsable.

underscore simple

02° Utiliser le code suivant dans lequel on a crée les autres assesseurs et où les attributs ont un underscore devant leur nom précédent. Lancer le sous IDLE puis utiliser les commandes présentées en dessous dans le Shell.

#!/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 get_name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    def get_first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    def get_level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    def get_class(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

>>> perso1 = Personnage()

>>> perso1._nom

>>> perso1.get_name()

Vous devriez obtenir ceci :

>>> perso1 = Personnage()

>>> perso1._nom

'nobody'

>>> perso1.get_name()

'nobody'

On constate donc bien que les deux façons de faire fonctionnent encore. La présence du underscore ne bloque rien. Il s'agit juste d'une convention qui indique au codeur qu'il doit considérer cette variable comme privée et ne pas y accéder directement : nommer l'attribut nom ou _nom ne change RIEN pour l'interpréteur Python. C'est un simple message humain envoyé à la personne qui va modifier le code.

underscore simple

Et ... sinon ... ça sert à quoi de faire ça ? Ben oui : on peut accéder directement aux valeurs.

2.3 Attribut privé : l'underscore double

On peut aller un peu plus loin et forcer le masquage. Il suffit de placer deux underscores devant les variables privées.

underscore double

#!/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 get_name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self.__nom)


    def get_first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self.__prenom)


    def get_level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self.__niveau)


    def get_class(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self.__classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self.__nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

03° Lancer encore une fois ce nouveau code puis utiliser ceci dans le Shell :

>>> perso1 = Personnage()

>>> perso1.get_name()

>>> perso1.__nom

Vous devriez obtenir ceci :

>>> perso1 = Personnage()

>>> perso1.get_name()

'nobody'

>>> perso1.__nom

Traceback (most recent call last):

  File "<pyshell#11>", line 1, in <module>

    perso1.__nom

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

Cette fois, ça fonctionne mieux en terme de masquage : on ne peut pas voir la variable en y accédant directement depuis l'extérieur de la classe.

Depuis l'intérieur, tout marche par contre normalement : on peut utiliser self.__name par exemple. D'ailleurs, vous pouvez constatez que dans la méthode combat, je continue à utiliser l'accès direct à self.__nom et à adversaire.__nom plutôt que de passer par l'assesseur. Je suis dans un code interne à Personnage, je peux y accéder directement aux attributs de l'instante en cours ET de toutes les autres instantes de la classe si je veux.

underscore double

Par contre, si je fais pareil depuis l'extérieur de la classe : ça déclenche une erreur, ce qui n'est pas forçément sympa non plus ...

Il existe deux écoles en Python avec l'utilisation de ces attributs cachés avec underscore double:

  • Ceux qui les utilisent
  • Ceux qui ne les utilisent pas car ils trouvent qu'un seul underscore est déjà bien suffisant : si quelqu'un veut vraiment détourner votre classe, il peut avoir accès à celle-ci en utilisant le dictionnaire, un ensemble que nous verrons un peu plus tard.

04° Auriez-vous une idée pour donner un intérêt à ce travail de définitions de méthodes assesseurs que nous venons de faire (que ce soit avec un nom, _nom ou __nom) ?

...CORRECTION...

Plusieurs programmes peuvent utiliser votre classe Personnage.

Si un jour vous vouliez mettre à jour votre méthode et faire de votre attribut niveau non plus une simple variable mais un élément de liste ou un string ou le renommmer en level car ça fait plus classe, vous mettez en péril l'ensemble des programmes qui ont fait confiance à votre Classe.

Ici, vous être maître de votre méthode. Vous pourriez renommer niveau en level : il vous suffit de changer return(level) et l'utilisateur final ne se rendra même pas compte que le moteur a changé !

Si on devait résumer le principe d'encapsulation, disons que :

  1. L'utilisateur ne doit pas avoir besoin de connaitre les noms des variables internes.
  2. L'utilisateur ne doit pas pouvoir lire directement les variables internes : il doit utiliser une méthode créée par les codeurs.
  3. L'utilisateur ne doit pas pouvoir modifier directement les variables internes : il doit utiliser uniquement une méthode créée pour cela. Cette méthode devra s'assurer que la nouvelle valeur est valide avant de modifier réellement la variable.

En une phrase : l'utilisateur ne doit pouvoir interagir avec l'objet qu'à travers les méthodes que vous lui avez fourni.

2.4 Les propriétés

Vu de l'extérieur, les objets-images de Pillow respectent presque le principe d'encapsulation : vous ne savez absolument pas comment est codée la classe Image mais vous avez déjà manipulé les objets-images à l'aide des attributs et méthodes qui vous ont été fournis par les personnes qui ont écrit le code.

Néanmoins, avec les objet-images de Pillow, nous avions utilisé des codes du type :

Pour lire la hauteur en pixels d'un objet-image nommé x : x.height

Par contre, ceci ne fonctionnait pas :

Pour modifier la hauteur en pixels d'un objet-image nommé x : x.height = 30 NE FONCTIONNE PAS. Il vaut utiliser une méthode nommée resize().

Comment est-il possible d'avoir accès en lecture à height mais pas en lecture ?

Regardons le code source de l'objet Image de Pillow :

def __init__(self):

    self.im = None

    self.mode = ""

    self.size = (0, 0)

    self.palette = None

    self.info = {}

    self.category = NORMAL

    self.readonly = 0

    self.pyaccess = None

Zut : pas un height ou un width à l'horizon. On retrouve bien le tuple size, mais c'est bien tout...

Il est donc temps de vous dire la vérité sur le fameux x.height : height n'est pas un attribut mais une propriété.

Une propriété est une sorte de façon de créer un accés avec un "faux" attribut. On trouve ceci sous la méthode __init__ précédente :

def __init__(self):

    self.im = None

    self.mode = ""

    self.size = (0, 0)

    self.palette = None

    self.info = {}

    self.category = NORMAL

    self.readonly = 0

    self.pyaccess = None


@property

def width(self):

    return self.size[0]


@property

def height(self):

    return self.size[1]

Si on y regarde de plus près, on voit donc qu'une propriété est une sorte de méthode qu'on va appeler sans parenthèse et qu'on doit configurer pour savoir ce qu'elle doit renvoyer via le return.

Ici, on on voit que si on tape mon_image.width, en réalité, on active la méthode-propriété width() qui renvoie la valeur stockée dans la première case 0 de l'attribut size via l'instruction self.size[0].

Mais on peut y mettre ce qu'on veut et renvoyer ce qu'on veut, comme avec une simple méthode assesseur qu'on aurait pu nommée get_width() comme dans la partie juste au dessus.

Passons à la pratique en utilisant les propriétés sur Personnage avec le code suivant :

#!/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()


    @property

    def name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

Quelques notes sur ces lignes de code :

En image, cela revient à faire ceci (la simple différente est la disparition des parenthèses) :

utilisation des propriétés

05° Lancer encore une fois ce nouveau code puis utiliser ceci dans le Shell :

>>> perso1 = Personnage()

>>> perso1.name

>>> perso1.first_name

Vous devriez obtenir ceci :

>>> perso1 = Personnage()

>>> perso1.name

'nobody'

>>> perso1.first_name

'nobody'

Nous avons donc bien accès aux valeurs contenues dans x._nom en passant par x.name. Voyons si on a bien protéger notre attribut _nom en écriture en laissant à l'utilisateur la connaissance de la propriété name.

06° Lancer les commandes suivantes :

>>> perso1.name = "Skywalker"

>>> perso1.first_name = 'Bob'

Voilà. Normalement, ça ne fonctionne pas : la propriété n'est définie par défaut qu'en écriture : vous pouvez donc laisser l'utilisateur lire les valeurs de vos attributs en lui donnant les informations sur les propriétés. Il ne pourra ainsi pas placer directement des valeurs non controlées dans vos attributs et casser votre classe.

>>> perso1.name = "Skywalker"

Traceback (most recent call last):

    File "<pyshell#8>", line 1, in <module>

        perso1.name = "Skywalker"

AttributeError: can't set attribute

Pourquoi ?

L'interpréteur vous le dit : vous n'avez pas créé de méthodes permettant de modifier la valeur, set en anglais. Nous verrons dans la partie 3 comment faire ceci.

Propriété name ou méthode get_name() ? : nous aurions très bien pu laisser la méthode get_name() en même temps que la propriété name. Mais ce sont deux façons de faire la même chose. Dans un programme, ce n'est pas une bonne chose. J'ai donc remplacé les méthodes get_ par les propriétés. Mais, je le répète, techniquement rien ne vous empêche de placer les deux manières de faire.

les 3 lectures possibles

2.5 Méthode assesseur sur un attribut-objet

Et pour les attributs internes qui sont des objets (comme arme et armure) ?

Il ne faut pas oublier que lorsqu'on transmet un objet, on transmet sa référence et pas son contenu comme dans le cas d'une variable 'simple'.

L'utilisation d'un assesseur va donc renvoyer directement la référence-mémoire de l'objet et on pourra y accéder directement !

J'ai crée une propriété assesseur weapon. La manipulation vise à vous montrer qu'avec un attribut-objet comme arme, la simple utilisation d'un assesseur ne protège absolument pas d'une modification directe !

#!/usr/bin/env python

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

import random


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

# Déclarations des classes

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

class Arme(object):

    "Cette classe contient les infos sur une arme"


    def __init__(self):

        self.nom='poings'

        self.degats=1

        self.type='[arme]'


    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(object):

    "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(object):

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


    @property

    def name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


    @property

    def weapon(self):

        "Renvoie une référence vers l'arme du personnage"

        return(self.arme)


    @property

    def armor(self):

        "Renvoie une référence vers l'armure du personnage"

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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

07° Lancer le code via IDLE et utiliser le shell pour lancer les instructions suivantes :

>>> perso2=Personnage()

>>> perso2.presentation()


>>> x = perso2.name

>>> x = "Alfred"

>>> perso2.presentation()


>>> y = perso2.weapon

>>> y.nom="Tout petit tank"

>>> perso2.presentation()

Les explications :

>>> perso2=Personnage()

>>> perso2.presentation()

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

Ici on crée une instance de Personnage nommée perso2.

On affiche simplement le contenu de cette instance.

>>> x = perso2.name

>>> x = "Alfred"

>>> perso2.presentation()

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

On affecte un contenu à x en y placant le retour du perso2.name : on obtient donc le contenu de _nom lorsqu'on regarde la définition de la propriété name.

On modifie depuis l'extérieur le contenu de x en y plaçant le string "Alfred".

On constate en affichant perso2 que le vrai nom de perso2 n'a pas été modifié. C'est normal, x contenait uniquement la valeur de _nom, pas la référence-mémoire.

>>> y = perso2.weapon

>>> y = "Tout petit tank"

>>> perso2.presentation()

nobody nobody

aucune classe de niveau 0

Arme : Tout petit tank qui fait 1 dégats à la distance : contact

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

On affecte un contenu à y en y placant le retour du perso2.weapon : on obtient donc la référence vers arme qui est un objet.

Lorsqu'on tape y.nom, on est donc capable d'aller modifier directement le contenu-mémoire du nom de l'objet arme à l'aide de son alias y.

On constate ainsi qu'on a bien réussi à modifier l'objet arme en partant de l'utilisation d'une propriété !

Dans ce cas, on ne peut plus dire qu'on respecte le principe de l'encapsulation...

Comment faire alors ?

Cela dépend de votre projet. Ici, le programme principal a-t-il besoin de l'objet de classe Arme ou simplement du nom de l'arme ? Si nous prenons le second cas, nous pourrions modifier ainsi la propriété weapon :

    @property

    def weapon(self):

        "Renvoie le nom de l'arme du personnage"

        return(self.arme.nom)

Et là, plus de problème : on renvoie une simple valeur vers le programme principal : le nom de l'arme. On donne donc plus de référence, plus moyen d'accéder au contenu mémoire à l'aide de la référence.

Attention donc de vous souvenir de ceci : lorsqu'on transmet un objet via un return, on transmet la référence et on crée donc une possibilité de modifier réellement l'objet à l'aide de cette référence.

3 - Méthodes mutateurs

C'est le même principe mais dans l'autre sens : il s'agit de forcer l'utilisateur à utiliser vos méthodes pour modifier le contenu des attributs de la classe.

Ici, c'est même beaucoup plus important : lorsqu'un utilisateur veut modifier le contenu du nom du personnage par exemple, il faut déjà vérifier qu'il envoie bien un string et pas un integer. Sinon, ça casse tout.

Par convention, les assesseurs commencent par get.

Par convention, les mutateurs commencent par set.

3.1 Création d'un mutateur set_x

Ainsi dans le programme principal, on pourra écrire : perso1.set_name("Alfred") et on placera une méthode mutateur dans la classe Personnage en commençant sa définition par def set_name(self,x):x est le paramètre qui recevra la valeur "Alfred" envoyée en argument lors de l'appel.

mutateur

08° Rajouter la méthode mutateur set_name pour l'attribut _nom dans la classe Personnage. Expliquer ce que fait ce mutateur.

def set_name(self,x):

    "Modifie le nom du personnage si le paramètre est bien un string"

    try :

        x = str(x)

        self._nom = x

    except:

        return(False)

    return(True)

...CORRECTION...

Le mutateur récupère l'argument fourni en le placant dans le paramètre x.

Il tente de convertir x en string en utilisant la fonction str. S'il y parvient, il place la chaîne dans self._nom.

Sinon, il renvoie False de façon à signaler qu'on a pas réussi à générer de string à l'aide de x.

09° Lancer votre nouvelle classe puis appliquer le code suivant dans la console Python, le Shell.

>>> perso4 = Personnage()

>>> perso4.set_name("Alfred")

>>> perso4.name


>>> perso4.set_name(45E-3)

>>> perso4.name


>>> perso5 = Personnage()

>>> perso4.set_name(perso5)

>>> perso4.name

Le résultat en image avec quelques explications :

>>> perso4 = Personnage()

>>> perso4.set_name("Alfred")

True

>>> perso4.name

'Alfred'

On crée une instance perso4 de Personnage puis on utilise le mutateur pour changer son nom. Le mutateur renvoie True pour indiquer que l'opération a réussi. C'est un choix. Nous aurions pu choisir de ne rien renvoyer vers le programme principal.

On notera la présence des guillemets car nous sommes dans le Shell : Python vous affiche bien la variable, pas simplement son contenu. Si vous tapiez print(perso4.name) vous auriez simplement Alfred, sans les guillemets pour préciser que c'est un string.

>>> perso4.set_name(45E-3)

True

>>> perso4.name

'0.045'

Même principe mais on envoie un float en argument.

La méthode set_name() fait son travail : elle tente de transformer votre réel en string, d'où le '0.045' qui correspond bien à 45.10-3.

>>> perso5 = Personnage()

>>> perso4.set_name(perso5)

True

>>> perso4.get_name()

'<__main__.Personnage object at 0x000001FB7D7A2BE0>'

>>> perso4.name

'<__main__.Personnage object at 0x000001FB7D7A2BE0>'

Cette fois, ça fonctionne encore mais on place vraiment n'importe quoi dans le nom de perso4.

On donne une instance en argument et la méthode mutateur parvient à la transformer en string en utilisant la méthode par défaut : on obtient ici le string <__main__.Personnage object at 0x000001FB7D7A2BE0>.

Comme quoi, le mutateur mérite encore quelques tests sur la viabilité du paramètre x...

Vous pouvez vous amuser à taper ceci dans la console pour voir comment faire :

>>> type(45) is int

True

>>> type("Alfred") is float

False

>>> type("Alfred") is str

True

>>> type(perso4) is Personnage

True

>>> isinstance(perso4, Personnage)

Remarque : J'ai associé la propriété name en lecture avec la méthode mutateur set_name() en écriture mais j'aurai pu laissé la méthode set_name() avec la méthode get_name() :

get_name() et set_name()

    ### Méthode des méthodes get_name() et set_name() ###


    def get_name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    def set_name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)

On peut alors taper des instructions cohérentes (2 méthodes) entre elles pour lire et modifier indirectement l'attribut _nom :

# Méthode 1 cohérente de lecture/écriture #

perso.get_name()

perso.set_name("Alfred")

3.2 Utilisation des propriétés

Néanmoins, il existe une autre méthode pour créer un mutateur : on peut utiliser les propriétés pour créer un code simple pour modifier _nom : nous allons voir qu'on peut configurer la propriété pour qu'elle accepte perso.name = "Alfred".

Nous allons ainsi pouvoir avoir une méthode cohérente (deux "attributs") pour lire et modifier indirectement l'attribut _nom :

Propriété en lecture et écriture

# Méthode 2 cohérente de lecture/écriture #

perso.name

perso.name = "Alfred"

Il suffit de completer le code sous la propriété name :

    @property

    def name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    @name.setter

    def name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)

Comme vous le voyez, on précise la création d'un mutateur-propriété en utilisant l'indication @name.setter : on donne ici le nom de la propriété déjà créée, on ne note pas @property.setter.

Sous cette indication, on replace le même code que celui de la méthode set_name() mais on la nomme simplement name.

Le code complet :

#!/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()


    @property

    def name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    @name.setter

    def name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

10° Lancer votre nouvelle classe puis appliquer le code suivant dans la console Python, le Shell.

>>> perso4 = Personnage()

>>> perso4.name = "Alfred"

>>> perso4.name

Cela devrait vous convaincre que ça fonctionne.

3.3 Utilisation des propriétés en partant des méthodes get_() et set_()

Il existe une autre façon de définir l'assesseur et le mutateur via une propriété, surtout si vous avez déjà défini une méthode assesseur get_name() et une méthode mutateur set_name() :

name = property(get_name, set_name)

Le code complet :

#!/usr/bin/env python

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

import random


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

# Déclarations des classes

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

class Arme(object):

    "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(object):

    "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(object):

    "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 get_name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    def set_name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)


    name = property(get_name, set_name)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

11° Vérifier que cela fonctionne en lançant le programme puis en tapant ceci dans le shell :

>>> perso4 = Personnage()

>>> perso4.name = "Alfred"

>>> perso4.name

>>> perso4.set_name("Darth")

>>> perso4.get_name("Darth")

Nous obtenons ceci :

>>> perso4 = Personnage()

>>> perso4.name = "Alfred"

>>> perso4.name

'Alfred'

>>> perso4.set_name("Darth")

>>> perso4.get_name("Darth")

'Darth'

Ca marche même trop bien : il y a deux manières de lire _nom et deux manières d'écrire dans _nom :

4 d'un coup !

Comment faire pour éliminer les méthodes get_name() et set_name() ?

Résumé

Les méthodes permettant de renvoyer le contenu d'un attribut sont nommmées assesseurs.

On peut signaler avec un underscore simple qu'on désire qu'un attribut soit utilisé comme un attribut privé. Il s'agit d'une convention forte : la majorité des gens vont suivre votre indication.

On peut imposer avec un double underscore de ne pas pouvoir accéder directemetn à un attribut. Néanmoins, c'est globalement une mauvaise idée car cela provoque un erreur en cas d'accès direct et il existe un moyen de passer outre avec l'utilisation de dictionnaire (voir activité sur les structures de données).

On peut créer une méthode assesseur. Elle commence généralement par get_.

On l'utilise en tapant par exemple x.get_name().

def get_name(self):

...

Mais on peut également créer une propriété qui permettra d'accéder à un attribut interne

On utilise alors par exemple x.name sans les parenthèses.

@property

def name(self):

...

On peut faire la même chose avec les mutateurs, qui permettront de modifier les attributs.

On peut créer une méthode mutateur. Elle commence généralement par set_.

On l'utilise en tapant par exemple x.set_name("Bob").

def set_name(self,x):

...

Mais on peut également créer une propriété qui permettra d'accéder à un attribut interne

On utilise alors la définition donnée par la lecture et on compète. On utilise par exemple x.name = "Bob" sans les parenthèses.

@name.setter

def name(self):

...

On évite habituellement de mélanger les deux façons d'utiliser la lecture et l'écriture.

On peut néanmoins utiliser les méthodes _get et _set pour créer une propriété :

name = property(_get_name, _set_name)

Voilà. C'est presque tout pour les assesseurs et les mutateurs.

N'oubliez pas que les buts des mutateurs est de vérifier les données à placer avant de les placer. On doit faire un maximum de tests sur elles de façons à être certain qu'elles soient compatibles avec le code de la classe.

S'il vous reste encore un peu de courage, il vous reste à découvrir les méthodes spéciales qui permettent de gèrer encore plus finement les assesseurs et les mutateurs. C'est dans l'activité suivante.

Ici, il ne nous reste qu'à voir la façon dans la documentation est créée automatiquement : en effet, pour qu'un utilisateur puisse utiliser votre classe, encore faut-il qu'il sache comment elle s'utilise.

4 - Gérer la documentation

Nous allons d'abord travailler avec la dernière classe Personnage. Celle où nous définissons des propriétés assesseurs pour tous les attributs, sauf pour _nom pour lequel nous avons créé une propriété permettant la lecture ET l'écriture. Nous avions utilisé une méthode get_name(), une méthode set_name() et une propriété définie en fonction de ces deux méthodes.

Si vous avez modifié des choses entre temps, voilà le code de base :

#!/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 get_name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    def set_name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)


    name = property(get_name, set_name)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

12° Lancer le code puis taper ceci dans la console Python de façon à voir apparaître la documentation créée à l'aide de nos commentaires :

>>> help(Personnage)

Help on class Personnage in module __main__:


class Personnage(builtins.object)

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

|

|     Methods defined here:

|

|     __init__(self)

|         Initialize self. See help(type(self)) for accurate signature.

|

|     combat(self, adversaire)

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

|

|     get_name(self)

|         Renvoie une variable contenant le nom du personnage

|

|     perdre_un_niveau(self)

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

|

|     presentation(self)

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

|

|     set_name(self, x)

|         Modifie le nom du personnage si le paramètre est bien un string

|

|     ----------------------------------------------------------------------

|     Data descriptors defined here:

|

|     __dict__

|         |dictionary for instance variables (if defined)

|

|     __weakref__

|         list of weak references to the object (if defined)

|

|     class_per

|         Renvoie une variable contenant la classe du personnage

|

|     first_name

|         Renvoie une variable contenant le prenom du personnage

|

|     level

|         Renvoie une variable contenant le niveau du personnage

|

|     name

|         Renvoie une variable contenant le nom du personnage

On remarquera que :

Premier constat : pour répondre au problème B, le mieux est de simplement revoir la façon dont la propriété name est créée : autant passer par @property pour l'assesseur et @name.setter pour le mutateur.

Pour répondre aux autres manques, il va falloir écrire des informations. Certainement sur plusieurs lignes. Comment ?

En utilisant une documentation multiligne à l'aide de """ en ouverture et """ en fermeture.

Voici quelques améliorations possibles permettant d'avoir une documentation un peu mieux construite. Mais le chemin est encore long pour obtenir quelque chose de distribuable !

Voici le code :

#!/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

    Vous pouvez accéder aux propriétés suivantes :

    name (lecture et écriture) : contient le nom du personnage

    first_name (lecture) : contient le prénom du personnage

    level (lecture) : contient le niveau du personnage

    class_per (lecture) : contient la classe du personnage

    

    Vous avez également accès aux attributs suivants :

    arme : contient la référence vers une instance de la classe Arme.

    armure : contient la référence vers une instance de la classe Armure."""


    def __init__(self):

        self._nom='nobody'

        self._prenom='nobody'

        self._niveau=0

        self._classe='aucune classe'

        self.arme = Arme()

        self.armure = Armure()


    @property

    def name(self):

        "Renvoie une variable contenant le nom du personnage"

        return(self._nom)


    @name.setter

    def name(self,x):

        "Modifie le nom du personnage si le paramètre est bien un string"

        try :

            x = str(x)

            self._nom = x

        except:

            return(False)

        return(True)


    @property

    def first_name(self):

        "Renvoie une variable contenant le prenom du personnage"

        return(self._prenom)


    @property

    def level(self):

        "Renvoie une variable contenant le niveau du personnage"

        return(self._niveau)


    @property

    def class_per(self):

        "Renvoie une variable contenant la classe du personnage"

        return(self._classe)


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

                test = False

            elif valeur2>valeur1:

                perdant = self._nom

                self.perdre_un_niveau()

                test = False

        return(perdant)

13° Relancer et effecuter un nouveau help(Personnage) : vous devriez constater qu'on parvient bien à donner des informations sur plusieurs lignes.

N'oubliez pas que la documentation vous permet également de vous souvenir de ce que fait votre code, même plusieurs mois ou années plus tard. N'hésitez jamais à en placer. Et en plus, ça peut servir d'aide-mémoire le jour de l'oral !

Voilà. C'est fini pour les classes. Pour les plus courageux, il reste néanmoins l'activité suivante sur les méthodes spéciales. Mais c'est vraiment juste pour vous montrer ce qu'on peut faire avec les classes.