python méthodes

Identification

Infoforall

23 - Les méthodes dans les objets


Nous avons vu qu'un objet est une sorte de conteneur permettant d'accéder à :

  • Des variables d'instance qui sont l'un de types possibles des attributs (voir bilan de cours). On les nomme aussi variables propres
  • Des fonctions membres qu'on nomme également méthodes
objet : conteneur à variables et fonctions

Prérequis : la première activité sur les objets

Logiciel nécessaire pour l'activité : Python 3 : Thonny, IDLE ...

Evaluation ✎ : questions 07-08-09-11-14-17-18

Résumé : Version HTML ou fond blanc ou ou PDF (couleur ou gris)

1 - Rappel

Cette partie est destinée à être lue et commentée au tableau.

a - Classe

Une Classe est une structure qu'il faut définir au préalable, une sorte de mode d'emploi permettant ensuite de créer des instances de cette classe, des objets "réels".

Vous pouvez reprendre l'analogie suivante :

  • La Recette de cuisine est la Classe
  • Les gateaux créés à partir de la Recette sont les instances de la recette, des objets.

On retiendra qu'on crée une Classe en utilisant le mode clé class suivi du nom de la Classe qui commencera par une Majuscule par convention.

L'intérieur de son code va contenir les instructions permettant de construire et préciser le contenu des objets.

1 2 3 4 5 6 7 8
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom, pre, niv, cla) : self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla

b - Instanciation : création d'une instance, un objet

Une fois la Classe en mémoire, on peut créer des objets basées sur cette structure.

Pour cela, on a besoin d'utiliser un Constructeur. En Python, on l'active en utilisant le nom de la classe sous forme d'une fonction : on rajoute des parenthèses. Comme toute fonction, elle peut nécessiter ou non l'envoi d'arguments.

Néanmoins, pour savoir si un Constructeur nécessite ou non l'envoi d'arguments à placer dans des paramètres, il faut aller voir le code de la méthode spéciale __new__ (la méthode constructeur) si elle existe ou de la méthode __init__ (la méthode initialisateur) si elle existe.

On notera que dans de nombreuses documentations, on fait comme si la méthode __init__ était le constructeur.

Cas particulier : le paramètre self est automatiquement transmis lors de l'activation d'une méthode : il contient l'identifiant-mémoire de l'objet. Il est inutile de le transmettre : il est transmis à la méthode automatiquement.

Exemple

1 4
class Personnage : def __init__(self, nom, pre, niv, cla) :

Lors de l'instanciation, le Constructeur a besoin de recevoir 4 arguments.

Ce Constructeur fait trois choses : il crée l'objet, active la méthode-initialisateure si elle existe puis renvoie vers le programme l'adresse du nouvel objet.

1
heros = Personnage("McClane", "John", 10, "Encaisse tout")

Pour savoir dans quels attributs ces paramètres vont être stockés, il faut aller voir le code contenu dans la méthode spéciale __init__.

4 5 6 7 8
def __init__(self, nom, pre, niv, cla) : self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla

On voit que "John" va être attribué au paramètre pre. Et ce paramètre va être attribuer à l'attribut prenom à l'aide de l'instruction suivante :

5
self.prenom = pre

Il faut juste se souvenir que self contient l'adresse de l'objet. Cela veut dire donc :

  1. Va voir à l'adresse de cet objet(self)
  2. et trouve (.)
  3. l'adresse de l'attribut qui se nomme prenom
  4. et affecte lui =
  5. ce qui se trouve à pre

c - Utilisation des attributs et des méthodes

La codification est toujours la même :

  • Récupération du contenu d'un attribut : objet.attribut. Vous pourrez alors faire ce que vous voulez du contenu : l'utiliser pour un calcul, le placer dans une variable...
  • Affectation d'un attribut : objet.attribut = nouvelle_valeur.
  • Utilisation d'une méthode (même spéciale) : objet.methode(...)

Exemple (avec la méthode __init__ puisqu'on en a pas défini d'autres pour l'instant)

1 2 3 4 5 6 7 8
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom, pre, niv, cla) : self.nom = nom self.prenom = pre self.niveau = niv self.classe = cla
>>> heros = Personnage("McClane", "John", 10, "Encaisse tout") >>> heros.prenom 'John' >>> heros.prenom = 'bob' >>> heros.prenom 'bob' >>> id(heros) 140441285082528

On peut également utiliser directement la méthode spéciale sur notre instance si on veut changer les caractéristiques. Vous allez voir que lorsqu'on fait appel à une méthode, on n'a pas besoin de fournir d'arguments pour le paramètre self. Ce paramètre est automatiquement fourni lors de l'appel.

>>> heros.__init__("Darth", "Vador", 28, "Casque Noir") >>> heros.prenom 'Vador' >>> id(heros) 140441285082528

Nous sommes bien parvenus à modifier l'objet après création grace à l'appel de la méthode et l'identifiant de l'objet n'a pas été modifié. Seul son contenu a changé.

d - Paramètre nommés et par défaut

Ces deux notions peuvent être utilisées également avec les fonctions. Mais elles sont très utiles lorsque on utilise un Constructeur puisqu'on peut

  1. Transmettre les paramètres dans l'ordre qu'on veut
  2. Ne pas transmettre certains paramètres. On utilise alors les valeurs par défaut qu'on trouve dans la méthode-initialisateur __init__.
1 2 3 4 5 6 7 8
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe

On peut alors utiliser le constructeur sous différentes formes :

10 11 12
heros = Personnage("McClane", "John", 10, "Encaisse tout") heros2 = Personnage(prenom="Junior", nom="MacClane", classe="Enfant", niveau=4) heros3 = Personnage(nom="Darth", prenom="Vador")

Ligne 10 : on envoie les paramètres dont le bon ordre sans les nommer.

Ligne 11 : on envoie les paramètres dans l'ordre qu'on veut car on les nomme.

Ligne 12 : on envoie certains paramètres nommés mais pas tous. Les paramètres non présents vont alors être initialisés à la valeur par défaut.

On notera que les paramètres ont exactement ici le même nom que les attributs. C'est une pratique courante. Mais ce n'est pas de la magie. Ce n'est pas parce que la méthode __init__ reçoit un paramètre nommé qu'il y a automatiquement création d'un attribut qui porte le même nom. Il faut alors voir les lignes 5 à 8 pour comprendre ce qu'on stocke ou pas.

2 - Déclaration des méthodes

Vous l'avez vu avec la méthode-initialisateur __init__, déclarer une méthode revient à déclarer une fonction à l'intérieur de la déclaration de la Classe.

Deux petites différences avec une fonction classique :

  1. Comme c'est à l'intérieur de la Classe, il faut penser à tabuler pour que l'interpréteur Python comprenne que vous déclarez une fonction qui doit être dans la Classe.
  2. Le premier paramètre est celui qui va recevoir automatiquement l'identifiant-mémoire de l'objet. En Python, on le nomme souvent self. Pourquoi ? En anglais, cela fait référence à l'objet lui-même : himself (lui-même), myself (moi-même)...

01° Analyser le code suivant.

Sans lancer le code :

  • Combien de points de vie (attribut pv) aura le personnage heros ?
  • Que fait la méthode mystere exactement ?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# Déclaration des Classes class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe self.pv_max = self.niveau * 10 + 5 self.pv = self.pv_max def mystere(self, degats) : '''Mystère mystère''' self.pv = self.pv - degats # Programme principal heros = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit")

...CORRECTION...

    Lors de la création, on envoie un niveau correspondant à 10 (voir la ligne 20).

    Du coup, lors de l'initialisation, les points de vie maximum vont être égaux à 10*10+5, soit 105 (voir ligne 11).

    L'attribut pv est alors également initialisé à cette valeur de 105 sur la ligne 12.

    La méthode mystere diminue l'attribut correspondant aux points de vie du nombre correspondant à degats. Par contre, on ne touche pas aux points de vie maximum.

    En supposant que degats soit bien un entier car il n'y a aucune documentation.

02° Vérifier vos réponses à l'aide des instructions interactives suivantes :

>>> heros.pv ??? >>> heros.mystere(5) >>> heros.pv ???

...CORRECTION...

>>> heros.pv 105 >>> heros.mystere(5) >>> heros.pv 100
Méthode et paramètre self

Toute méthode doit comporter OBLIGATOIREMENT au moins un paramètre.

Il sera automatiquement rempli avec l'identifiant-mémoire de l 'objet qui vient d'activer la méthode.

La convention en Python est de le nommer self.

La syntaxe spécifique des objets permet donc d'écrire facilement ceci

>>> heros.mystere(5)

Lorsqu'on note ceci, l'interpréteur Python va donc lancer la méthode comme si nous avions tapé ceci : mystere(self=heros, degats=5)

Plus de détails dans la FAQ pour ceux qui veulent comprendre le mécanisme interne. Rien de magique.

03° Va-t-il se passer quelque chose de différent si on tape directement ceci dans la console ?

>>> heros.pv = heros.pv - 5

...CORRECTION...

Non : il se passe exactement la même chose. On diminue l'attribut pv de 5 points.

Bon, ça sert à quoi de créer une méthode qui va modifier notre attribut si on peut faire pareil avec une instruction ?

Question : laisseriez-vous quelqu'un toucher directement au moteur de votre voiture pour augmenter l'accélération de la voiture plutôt que d'appuyer directement sur la pédale d'accélération ? Trouveriez-vous malin de toucher à votre propre moteur alors que quelqu'un a déjà concu une pédale qui aura comme effet de faire exactement ce que vous voulez ?

C'est le principe de la programmation orientée objet (POO) : on fournit à l'utilisateur un code solide (la Classe) qui contient un ensemble de méthodes qui permettent de modifier de façon sécurisée les attributs de votre objet. Pas la peine de faire de la bidouille dans l'objet-voiture :

  • Pour diminuer l'attribut accélération de votre voiture, on crée une méthode-interface freins qui va diminuer de façon sécurisée l'attribut accélération.
  • Pour augmenter l'attribut accélération de votre voiture, on crée une méthode-interface accelerateur qui va augmenter de façon sécurisée l'attribut accélération.
Principe de l'encapsulation

En programmation orientée objet (POO) respectant l'encapsulation, on ne permet pas à un utilisateur de modifier directement les attributs de l'objet. D'ailleurs, l'utilisateur ne devrait même pas connaître les noms des attributs.

Tout ce que veut l'utilisateur, c'est provoquer un effet sur l'objet. Il n'a absolument pas à connaître comment l'objet est constitué à l'intérieur. Il doit juste savoir comment se servir de l'objet de façon sécurisée.

Les "méthodes d'interaction" constituent donc l'interface entre l'utilisateur et l'intérieur de l'objet lui-même (ses attributs).

Si on reprend l'analogie de la voiture, vous n'utilisez que les boutons, leviers et pédales (les "méthodes d'interaction") fournis par les ingénieurs ayant concu la voiture. Vous n'avez aucune connaissance exacte du contenu interne de la voiture (les attributs).

interaction entre objet et utilisateur

L'utilisateur ne voit que cela de la voiture : les moyens qu'il a d'agir sur elle. Il ne connait pas les attributs internes : température de moteur, puissance utilisée par le moteur, vitesse réelle de rotation du moteur...

Voici le schéma global pour le concepteur de la Classe qui lui connait le code dans sa totalité :

point_de_vue_concepteur.png

04° Réaliser la méthode d'interface modifier_pv : on veut rajouter le modificateur à la valeur courante de l'attribut pv. Ensuite, si ce nombre courant dépasse la valeur maximale pv_max, on le limite à cette valeur maximale.

De la même façon, le nombre de pv (points de vie) ne pourra pas devenir négatif.

On vous fournit un exemple dans la documentation. Votre code devra donc au moins passer le doctest utilisant cette documentation.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Attributs calculés à partir des autres paramètres self.pv_max = self.niveau * 10 + 5 self.pv = self.pv_max def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pass # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit")

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Attributs calculés à partir des autres paramètres self.pv_max = self.niveau * 10 + 5 self.pv = self.pv_max def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit")

05° Lancer le code (le votre ou celui de la correction) de façon à avoir la variable-objet heros en mémoire.

Taper les instructions ci-dessous de façon à vérifier qu'on parvient bien à agir sur cette variable depuis l'extérieur (ici l'interface permettant d'activer les méthodes d'interaction est le shell de Python).

>>> heros.pv >>> heros.modifier_pv(-10) 95

Python permet d'interagir comme on veut avec les attributs. Ce langage n'a pas intégré de système de protection sur les objets. Nous verrons plus loin que d'autres langages en possèdent.

06° Dire si les instructions suivantes sont :

  • Possible avec Python
  • Légitime si on tente de respecter le premier principe d'encapsulation qu'on vient de voir

On considère que votre code est en mémoire et qu'il existe donc bien une variable heros faisant référence à une instance de la classe Personnage.

Voici les instructions

  1. Instruction 1 : on tape cette instruction dans le Shell :
  2. >>> heros.pv = 50

  3. Instruction 2 : on place cela dans l'une des méthodes
  4. self.niveau = 50
  5. Instruction 3 : on place ceci en dehors du code de la Classe, par exemple sous la dernière ligne, celle où on crée la variable heros.
  6. self.niveau = 50
  7. Instruction 4 : on tape cette instruction dans le Shell :
  8. >>> self.pv = 50

  9. Instruction 5 : on tape cette instruction dans l'une des méthodes de la classe Personnage :
  10. heros.niveau = 50

...CORRECTION...

  1. Instruction 1 : on tape cette instruction dans le Shell :
  2. >>> heros.pv = 50

    Possible en Python.

    Par contre, cette instruction ne respecte pas l'encapsulation : on laisse l'utilisateur modifier l'objet sans passer par l'une des méthodes d'inferface créée pour vérifier et gérer sa demande.

  3. Instruction 2 : on place cela dans l'une des méthodes
  4. self.niveau = 50

    Possible en Python.

    Légitime en POO : on modifie les attributs de l'objet depuis l'intérieur de son propre code.

  5. Instruction 3 : on place en dehors du code de la Classe, par exemple sous la dernière ligne, celle où on crée la variable heros.
  6. self.niveau = 50

    La variable self n'est pas une variable magique. Lorsqu'on active une méthode, self contient la référence de l'objet oui. Mais là, on utilise juste une variable que se nomme self dans le corps du programme. Comme on l'a pas affecté, cela va provoquer une erreur. L'interpréteur Python va dire qu'il ne connaît pas de variable nommée self.

    Du coup, cette instruction n'est tout simplement pas possible en Python (à moins d'avoir créé un code volontairement tourdu possédant une variable self contenant l'adresse d'une).

  7. Instruction 4 : on tape cette instruction dans le Shell :
  8. >>> self.pv = 50

    Comme ci-dessous : la variable self est l'un paramètre des méthodes. Seule la méthode peut donc accéder à son paramètre. Aucune existence en dehors de la méthode, que ce soit dans le programme principal ou dans le Shell de l'interpréteur. Il suffit de regarder : vous ne verrez pas cette variable dans l'onglet VARIABLES de Thonny.

  9. Instruction 5 : on tape cette instruction dans l'une des méthodes de la classe Personnage :
  10. heros.niveau = 50

    C'est plutôt difficile ça, mais il faut bien le comprendre :

    • Si on regarde le code, on voit que la variable heros est bien définie dans le programme principal si on le lance directement (__name__ == '__main__').
    • Python permet à une fonction (et donc une méthode) d'aller lire le contenu d'une variable globale : notre méthode va donc pouvoir aller lire l'adresse de l'objet heros.
    • Comme on possède l'adresse de l'objet, on peut modifier son contenu par effet de bord !

    Moralité :

    • L'instruction est possible si heros existe dans le programme principal
    • L'instruction est légitime : c'est dans l'une des instances de la classe Personnage qu'on modifie l'instance heros. Par contre, on évitera d'agir ainsi puisque cela revient à agir sur une variable globale. Le mieux est clairement de passer la référence de l'instance heros en paramètre.
Do you speak English ?

En Français, on nomme cette méthode modifier_pv. En Anglais, on la nommerait set_pv. To set ayant ici le sens d'établir la valeur. Bien entendu, c'est un peu du franglais : pv (points de vie) serait plutôt traduit par ht (hit points). On aurait donc eu une méthode dont le nom aurait pu être set_hp

Vocabulaire : Mutateur (hors NSI)

On dit en Français que méthode modifier_pv est une méthode-mutateur. C'est une méthode qui permet de modifier l'attribut en vérifiant que la demande de modification est valide, que la modification finale est possible, qu'on modifie bien également les données qui dépendent de cet attribut...

Si on avait simplement tapé par exemple heros.pv = heros.pv + modificateur, il serait possible que les pv dépassent les pv maximum autorisés ou soient inférieurs à zéro.

L'intérêt des mutateurs est donc de rajouter des protections et des vérifications pour éviter que l'un des attributs ne fasse référence à une valeur "impossible" et que cela fasse planter l'objet.

Continuons à créer nos personnages et leur permettre d'interagir entre eux. Comme il s'agit d'un jeu de combat, cela va principalement consister à se mettre des coups sur la tête.

Les personnages font devoir avoir 4 nouveaux attributs qu'on veut initialiser de cette façon.

  1. Un attribut attaque qui va servir à savoir si le personnage parvient à porter un coup à un autre personnage.
  2. Un attribut esquive qui va servir à savoir si le personnage parvient à esquiver le coup qu'un autre personnage lui porte.
  3. Un attribut puissance qui va correspondre aux nombres de dégats effectifs si le coup est bien porté.
  4. Un attribut protection qu'on va retrancher aux dégats reçus par le personnage.

Ces attributs vont être calculés une première fois à la création du personnage. On décide donc de créer une méthode mise_a_jour qui va contenir l'ensemble des calculs et modifications à effectuer à chaque fois qu'un paramètre comme le niveau ou la classe est modifiée.

On notera que la méthode __init__ commence par lister les attributs (ça permet de connaître tous les noms déjà utilisés pour les attributs) puis fait appel à la méthode mise_a_jour pour calculer les valeurs des attributs qui dépendent de la classe et du niveau.

Voici le code sur lequel vous allez travailler :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def mise_a_jour(self) : '''Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

✎ 07° Expliquer pourquoi on trouve les valeurs suivantes dans le Shell après lancement du programme.

>>> heros1.pv 105 >>> heros1.protection 5 >>> heros2.pv 85 >>> heros2.protection 8

✎ 08° Expliquer pourquoi on doit placer self suivi d'un point sur la ligne 27.

✎ 09° Finaliser la méthode mise_a_jour.

  • Un "Jedi" possède son niveau en attaque, en esquive, en puissance et en protection. Cette classe de personnage est totalement déséquilibrée !
  • Un "Rebelle" possède son niveau en attaque et en puissance mais la moitié de son niveau en esquive et en protection.
  • Un "Stormtrooper" possède la moitié de son niveau partout. Ils tombent comme des mouches dans les films !
  • Pour n'importe quelle autre classe (même "Garde de Nuit"), on prendra 1/3 du niveau dans les 4 attributs. De toutes manières, un gars avec une épée face à un tir de blaster ou une épée laser, ça risque de ne pas durer longtemps.

Si vous n'avez pas réussi la question précédente, nous allons travailler avec une version simplifiée. Sinon, gardez la votre !

10° Finaliser la méthode subit_des_degats. Cette méthode reçoit un paramètre degats. Si degats est positif, on lance alors un appel intelligent à modifier_pv pour diminuer les pv du personnage.

Sinon, on ne fait rien : on ne peut pas subir de dégâts négatifs.

Vous pourrez tester de cette façon dans le Shell :

>>> heros1.subit_des_degats(10) >>> heros1.pv 95 >>> heros1.subit_des_degats(-20) >>> heros1.pv 95
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' pass def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0 : self.modifier_pv(-degats) def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

✎ 11° Rajouter la documentation (comprenant un exemple pour le module doctest). Vous pouvez vous inspirer du test de la méthode modifier_pv.

N'oubliez pas la ligne vide après le dernier test. Sinon doctest pensera qu'il y a autre chose à afficher sur la console.

Nous voudrions maintenant gérer les attaques d'un personnage sur un autre à travers la méthode veut_attaquer.

Pour simuler une attaque de heros1 sur heros2 , on voudra écrire ceci dans le Shell ou dans le programme principal :

>>> heros1.veut_attaquer(heros2)

Voici le prototype de la méthode :

1
def veut_attaquer(self, cible) :

12° Que contient le paramètre self lors de cet appel ? Que contient le paramètre cible ?

...CORRECTION...

>>> heros1.veut_attaquer(heros2)
1
def veut_attaquer(self, cible) :

Cela veut dire : va chercher l'adresse de l'objet heros1 et active sa méthode veut_attaquer.

Le paramètre automatique self fait donc référence à heros1.

Le paramètre cible fait donc référence lui à heros2.

Dans la suite des explications, 1d20 fait référence à un dé à 20 faces : il peut donc donner un résultat entier entier dans [1;20].

On considère la règle du jeu suivante :

  • Si (attaque de l'attaquant + 1d20) dépasse (esquive du défense + 10) :
    • on diminue les pv du défenseur de (20 + puissance de l'attaquant * 2 - protection du défenseur // 2)
  • Sinon si (attaque de l'attaquant + 1d20) dépasse l'esquive du défense :
    • on diminue les pv du défenseur de (10 + puissance de l'attaquant - protection du défenseur)

On notera que les dégâts doivent toujours être positifs quelque soit la protection de la cible !

13° Réaliser la méthode veut_attaquer pour qu'elle fasse le travail demandé. La perte de pv devra bien entendu être réalisée via la méthode subit_des_degats qui fait appel elle-même à modifier_pv.

Pour l'instant, la méthode ne fait rien, à part tirer un nombre aléatoire entre 1 et 20 et placer le résultat dans une variable.

Remarque : vous pouvez remplacer mise_a_jour par votre version si elle est fonctionnelle.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
import random class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def veut_attaquer(self, cible) : '''Le personnage lance une attaque contre la cible :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (None) :: ''' d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20] def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0 : self.modifier_pv(-degats) def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

Voici un exemple d'utilisation (pensez à utiliser la flèche VERS LE HAUT pour retrouver vos dernières commandes plutôt que de les retaper à la main) :

>>> heros1.veut_attaquer(heros2) >>> heros2.pv 80 >>> heros2.veut_attaquer(heros1) >>> heros1.pv 98 >>> heros1.veut_attaquer(heros2) >>> heros2.pv 58 >>> heros2.veut_attaquer(heros1) >>> heros1.pv 91 >>> heros1.veut_attaquer(heros2) >>> heros2.pv 53 >>> heros2.veut_attaquer(heros1) >>> heros1.pv 91

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
import random class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def veut_attaquer(self, cible) : '''Le personnage lance une attaque contre la cible :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (None) :: ''' d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20] if (d20 + self.attaque) > (10 + cible.esquive) : degats = 20 + self.puissance*2 - cible.protection//2 cible.subit_des_degats(degats) elif (d20 + self.attaque) > cible.esquive : degats = 10 + self.puissance - cible.protection cible.subit_des_degats(degats) def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0 : self.modifier_pv(-degats) def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

Voilà pour la partie sur la création de méthodes.

On retiendra qu'une méthode est une fonction contenue dans une Classe et dans les instances de cette classe. Les deux différences fondamentales avec les fonctions :

  • L'appel se fait en tapant objet.methode(...)
  • Lors de la déclaration de la méthode, il faut rajouter un premier paramètre (nommé self par tradition, mais on pourrait le nommer choucroute) qui sera automatiquement associé à l'identifiant-mémoire de l'objet utilisé pour activer cette méthode.
  • Version classique d'une déclaration (tout le monde va comprendre ce qu'est self)

    1
    def veut_attaquer(self, cible) :

    Une fois qu'on a l'habitude, on sait que la variable locale qui contient la référence de l'instance est self. Pas la peine de chercher.

    Version qui paraît plus simple mais qui complique la lecture d'un habitué de Python en réalité

    1
    def veut_attaquer(attaquant, cible) :

3 - Privé-Public et Interface

Une partie rapide. Pas la peine de retenir, c'est pour la culture générale. Le but est de vous faire un peu réfléchir sur la notion de variables privées ou publiques.

Si on veut parler d'interaction utilisateur-objet, on pourrait dire que la seule méthode autorisée pour l'utilisateur dans le Shell est veut_attaquer. Les méthodes que l'utilisateur a le droit d'utiliser sont donc les méthodes d'interaction ou d'interface, qui sont également des méthodes publiques : des méthodes que tout le monde peut utiliser pour agir sur l'objet.

Les autres méthodes (__init__, modifier_pv, subit_des_degats) pourraient être considérées comme des méthodes que l'utilisateur n'a pas le droit d'activer directement. C'est la méthode publique qui va les activer ou non. On les nomme alors méthodes privées. On ne peut alors les utiliser que depuis l'intérieur du code de la Classe (concrétement, il faut y accéder via self.truc).

Dans Python, il n'y a pas vraiment possibilité de créer des attributs privés ou des méthodes privées (inaccessibles à l'utilisateur). Ce langage prend le parti que quelqu'un qui a accès au code source peut de toutes manières casser le fonctionnement du code s'il le désire.

D'autres langages (Java ou C++ par exemple) permettent de définir clairement certains attributs et méthodes comme étant privés.

Faire semblant d'être privé dans Python

Semblant car on peut détourner la protection sans problème si on a accés au code. Voir la FAQ pour plus de détails.

Pour "interdire" l'accès aux attributs ou aux méthodes qu'on estime être privés, il suffit de changer leurs noms : il faut placer deux underscores devant. Pas devant et derrière. Juste devant.

Exemple :

1 2 3 4 5 6 7 8 9 10 11 12 13
class Invisible : def __init__(self) : self.__invisible = "Je suis invisible de l'extérieur" self.visible = "Moi, tout le monde me voit" def affiche_toi(self) : print(self.__invisible) def __paspossible(self) : print("On ne peut m'activer que de l'intérieur !") def possible(self) : print("On peut m'activer de l'extérieur")

✎ 14° Utiliser les instructions Shell-Python suivantes pour bien voir la différence entre l'utilisation de deux underscores dans le nom d'un attribut ou d'une méthode.

Question : expliquer pourquoi on parvient à avoir un affichage lorsqu'on utilise la méthode affiche_toi ?

>>> toto = Invisible() >>> toto.visible 'Moi, tout le monde me voit' >>> toto.possible() On peut m'activer de l'extérieur >>> toto.__invisible AttributeError: 'Invisible' object has no attribute '__invisible' >>> toto.affiche_toi() Je suis invisible de l'extérieur >>> toto.__paspossible() AttributeError: 'Invisible' object has no attribute '__paspossible'

En réalité, cette pratique du préfixe __ est très rare en Python. Pourquoi ?

  1. Ca ne rend pas vraiment pas les variables privées
  2. Ca provoque une erreur lorsqu'on veut accéder à de telles variables

La vraie solution Python aux variables privées

Plutôt que de réellement créer un mécanisme de variables privées, Python a choisi de faire confiance aux gens qui interviennent sur le code.

Convention Python sur les attributs et méthodes "privées"

Lorsqu'un développeur veut signaler la présence d'attributs "privés" ou de méthodes "privées", il veut simplement dire qu'il ne faut pas utiliser ces attributs et ces méthodes depuis l'extérieur de la Classe.

On signale cela en nommant ces attributs et ces méthodes en rajoutant un simple underscore.

Exemple 1: méthode publique (c'est à dire qu'on peut l'utiliser depuis l'extérieur de la Classe, comme méthode d'interface par exemple)

def modifier_pv(self, modificateur) :

Exemple 2: méthode privée (c'est à dire : ne l'utilisez pas en tant que méthode-interface)

def _modifier_pv(self, modificateur) :

Exemple 3 : attribut public (c'est à dire qu'on peut lire et modifier cet attribut depuis l'extérieur de la Classe)

pv

Exemple 4 : attribut privé (c'est à dire qu'on vous demande de ne pas lire ni modifier cet attribut depuis l'extérieur de la Classe)

_pv

Comme vous le voyez, en Python, "on vous demande de".

D'autres langages permettent d'imposer ce type de comportement.

Attention : ne confondez pas avec les attributs et les méthodes qui ont un double underscore au début et à la fin.

__init__ est une méthode spéciale. Elle n'est pas "privée" du tout.

__name__ est un attribut de Classe spécial. Il n'est pas "privée" du tout. Il contient le nom de la Classe.

__dict__ est un attribut d'instance spécial. Il n'est pas "privée" du tout. Il contient un dictionnaire dont les clés sont les noms des attributs et les valeurs le contenu des attributs.

15° Relancer le programme de la correction de la question 13.

Utiliser ensuite l'instruction suivante dans le Shell pour observer le contenu de cet attribut spécial.

>>> heros1.__dict__ {'nom': 'Snow', 'prenom': 'John', 'niveau': 10, 'classe': 'Garde de Nuit', 'pv_max': 105, 'pv': 105, 'attaque': 3, 'esquive': 3, 'puissance': 3, 'protection': 5} >>> heros2.__dict__ {'nom': 'Skywalker', 'prenom': 'Luke', 'niveau': 8, 'classe': 'Jedi', 'pv_max': 85, 'pv': 85, 'attaque': 2, 'esquive': 2, 'puissance': 2, 'protection': 8}

4 - Encapsulation : accesseurs pour les attributs

Résumons

  1. Un objet est construit à partir de sa Classe, son moule en utilisant un Constructeur.
  2. Un objet est constitué d'attributs et de méthodes
  3. La variable d'un objet contient bien l'identifiant-mémoire d'un objet
  4. Le principe d'encapsulation indique qu'il ne faut pas laisser les utilisateurs manipuler directement les attributs et les méthodes de l'objet (de la même façon qu'on ne vous laisse pas toucher au moteur de votre voiture)
  5. Pour laisser l'utilisateur interagir avec l'objet, on a donc besoin de créer des méthodes d'interface (ou méthodes d'interaction objet-utilisateur) qui devront être utilisables depuis l'extérieur (on dit qu'elles sont publiques).
    • Les méthodes-mutateurs permettent à l'utilisateur de proposer de modifier certaines caractéristiques de l'objet. La proposition est alors gérer par le code de l'objet qui va valider ou non, appliquer ou non.
    • Les méthodes accesseurs de l'interface permettent à l'utilisateur d'interroger l'objet sur son état. Si l'encapsulation est bien réalisée, les attributs sont tous censés être privés : un utilisateur n'a pas besoin de savoir comment l'objet est défini à l'interne.

Des accesseurs : c'est ce qui nous manque pour notre classe Personnage. Pour l'instant, le seul moyen de connaître le nombre de PV est d'interroger directement l'objet :

>>> heros2.pv 80

On peut le faire en Python. N'oublions que ce langage est notamment utilisé pour créer des prototypes d'application : on veut savoir si une idée est réalisable, pas nécessairement la réaliser au final avec Python.

Voyons comment réaliser une méthode-accesseur facilement avec Python.

16° Compléter la méthode afficher_etat pour qu'elle renvoie la valeur de l'attribut pv de l'objet.

On considère ici que l'utilisateur utilise le Shell Python pour fournir ses intructions.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
import random class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def afficher_etat(self) : '''Affiche dans l'interface utilisateur l'état du personnage''' pass def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def veut_attaquer(self, cible) : '''Le personnage lance une attaque contre la cible :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (None) :: ''' d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20] if (d20 + self.attaque) > (10 + cible.esquive) : degats = 20 + self.puissance*2 - cible.protection//2 cible.subit_des_degats(degats) elif (d20 + self.attaque) > cible.esquive : degats = 10 + self.puissance - cible.protection cible.subit_des_degats(degats) def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0 : self.modifier_pv(-degats) def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
import random class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : ''' :: param nom(str) :: contient le nom du personnage :: param prenom(str) :: contient le prénom du personnage :: param niveau(int) :: un entier positif (donc 0 autorisé) :: param classe(str) :: l'une des classes autorisées ''' # Attributs stockant les paramètres reçus self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe # Liste des autres attributs associés self.pv_max = 0 # Valeur maximale de la vie self.pv = 0 # Valeur restante self.attaque = 0 # Capacité à porter un coup self.esquive = 0 # Capacité à esquiver un coup self.puissance = 0 # Force des dégats sur un coup porté self.protection = 0 # Réduction sur un coup reçu # Initialisation des attributs associés self.mise_a_jour() self.pv = self.pv_max def afficher_etat(self) : '''Affiche dans l'interface utilisateur l'état du personnage''' return self.pv def mise_a_jour(self) : '''VERSION SIMPLIFIEE - Calcule les valeurs des différents paramètres''' self.pv_max = self.niveau * 10 + 5 self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 if self.classe == 'Jedi' : self.protection = self.niveau else : self.protection = self.niveau // 2 def veut_attaquer(self, cible) : '''Le personnage lance une attaque contre la cible :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (None) :: ''' d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20] if (d20 + self.attaque) > (10 + cible.esquive) : degats = 20 + self.puissance*2 - cible.protection//2 cible.subit_des_degats(degats) elif (d20 + self.attaque) > cible.esquive : degats = 10 + self.puissance - cible.protection cible.subit_des_degats(degats) def subit_des_degats(self, degats) : '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0 : self.modifier_pv(-degats) def modifier_pv(self, modificateur) : '''Modifie l'attribut de pv en lui rajoutant modificateur (qui peut être négatif) :: param self(Personnage) :: l'instance sur laquelle on agit :: param modificateur(int) :: le modificateur, positif ou négatif :: return None :: "procédure" .. effet de bord :: modifie l'objet par effet de bord :: exemple pouvant servir à doctest :: >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 105 >>> heros_test.modifier_pv(50) >>> heros_test.pv 105 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 90 >>> heros_test.modifier_pv(-100) >>> heros_test.pv 0 ''' pv_modifies = self.pv + modificateur if pv_modifies < 0 : self.pv = 0 elif pv_modifies > self.pv_max : self.pv = self.pv_max else : self.pv = pv_modifies # Programme principal if __name__ == '__main__' : import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=10, classe="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, classe="Jedi")

L'intérêt de faire cela ?

  • Déjà, cela permet de lire le contenu d'un attribut sans connaitre son nom : c'est donc très utile pour l'encapsulation. Surtout si l'attribut est privé !
  • Ensuite, on peut modifier la valeur ou la tranformer. Par exemple transformer une vitesse en m/s en km/h. Une autre application concrète ci-dessous.

✎ 17° On veut maintenant que notre méthode renvoie un string :

  • "Pleine forme" si les PV dépasse 75% du maximum possible (ou indique vert si on avait une interface graphique)
  • "Blessé" si les PV sont inférieurs à 25% du maximum possible (ou une barre rouge dans une interface graphique)
  • Sinon "Fatigué"

✎ 18° Ecrire un code volontairement faux où une simple inversion de l'ordre des tests dans votre série de test IF ELIF ELSE ne permet pas d'obtenir la bonne réponse.

Nous voilà arrivés au bout de cette découverte de la programmation orientée objet.

Python permet de créer très facilement des objets et n'impose pas de restriction imposant de suivre l'encapsulation : vous avez le droit de toucher le moteur de votre voiture avec Python. Mais attention, vous risquez de vous faire mal aux doigts.

Dans la mesure du possible :

  • Créer un ensemble limité de méthodes permettant d'interagir avec votre Classe d'objets :
    • Créer des méthodes-mutateurs permettant à l'utilisateur de proposer des modifications et vérifier toujours que les modifications soient valides.
    • Demander lui plutôt d'utiliser les méthodes-accesseurs pour fournir l'état de l'objet, cela vous permettra de présenter l'information comme vous le voulez.

Pour revenir à l'exemple de la voiture, l'utilisateur de votre objet ne doit pas avoir à connaitre le code interne de votre Classe pour utiliser des instances. Il doit simplement connaître les méthodes utiles pour interagir avec lui de la façon la plus simple possible.

A vous de garantir la solidité de l'ensemble en produisant un code efficace et capable de filtrer les entrées-utilisateurs toxiques.

5 - Des objets partout

Le mot pour la fin : en Python, tout est objet.

Même les integers sont des objets !

Pour rappel, voilà comment tester le type d'une variable (pratique pour les Assertions)

>>> type(heros1) <class '__main__.Personnage'> >>> type(heros1) == Personnage True >>> isinstance(heros1, Personnage) True

Et oui. C'est le même fonctionnement qu'avec les types natifs : int, float, list, dict, tuple et complex.

Et, c'est également le cas des objets-fichiers que vous avez créés lors des activités sur CSV et l'OpenData.

Le constructeur est parfois un peu différent (fonction open) mais une fois passé le stade de la création, on retrouve le principe des objets.

Idem pour les interfaces graphiques Tkinter ou Turtle.

Comme quoi, ça fait maintenant un sacré bout de temps que vous manipuliez des objets sans le savoir.

6 - FAQ

Des explications sur le transfert de self ?

Rien de bien compliqué à comprendre en réalité.

Imaginons que vous ayez une classe Personnage contenant une méthode mystere.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# Déclaration des Classes class Personnage : "Ceci est une classe permettant de créer un personnage dans mon super RPG" def __init__(self, nom='Aucun', prenom='Aucun', niveau=0, classe="Humain de base") : self.nom = nom self.prenom = prenom self.niveau = niveau self.classe = classe self.pv_max = self.niveau * 10 + 5 self.pv = self.pv_max def mystere(self, degats) : '''Mystère mystère''' self.pv = self.pv - degats

Votre programme lance ces instructions :

1 2
heros = Personnage(nom="A', prenom="b", niveau=10) heros.mystere(15)

En ligne 1, on crée une instance de la classe Personnage et on stocke sa référence dans la variable heros.

En ligne 2, on utilise la méthode mystere appartenant à l'objet heros en lui transmettant un seul argument : 15.

Or, lorsqu'on regarde le code de la fonction, on voit que le prototype montre deux arguments :

14
def mystere(self, degats) :

Alors, ça marche comment le transfert automatique de l'adresse de l'objet à la méthode ?

Lorsque vous tapez ceci :

2
heros.mystere(15)

Python exécute ceci :

1 2
Personnage.mystere(self=heros, degats=15)

On lui dit donc d'aller dans la classe Personnage (remarquez bien l'absence de parenthèses) et d'exécuter la méthode mystere et cette fois ci, on lui fournit bien les deux paramètres.

Certains langages (comme Python) permettent vraiment de taper l'un ou l'autre code. Pour d'autres, la version avec accès à la classe n'est pas accessible mais le mécanisme interne reste le même.

Attention néanmoins : utilisez bien la méthode usuelle. Ce n'est pas parce que vous savez comment ça fonctionne réellement qu'il faut se passer de la version élégante d'agir sur l'objet.

On n'utilisera donc que :

2
heros.mystere(15)

Des méthodes avec un double underscores devant et derrière ?

Les méthodes dont le nom commençent par deux underscores sont des méthodes spéciales. Elles jouent un rôle particulier dans le sens où l'interpréteur Python les recherche automatiquement.

Si il les trouve dans le code, il va appliquer ce qu'elles préconisent dans certaines conditions.

Quelques exemples :

  • __new__() : on la recherche à la construction d'un nouvel objet. Elle peut alors remplacer le code habituel de construction.
  • __init__() : on la recherche après la construction d'un nouvel objet. Elle peut alors de configurer les valeurs des attributs...
  • __add__() : on la recherche lorsqu'on tape une instruction correspondant à l'addition de deux objets. Exemple : heros1 + heros2. On pourrait ainsi transformer cela en demande de combat par exemple. Nous verrons son utilisation dans le mini-projet sur le jeu de dés.
  • Et bien d'autres ! documentation Python

Des attributs avec un double underscores devant et derrière ?

Eux sont également spéciaux dans le sens où ils sont affectés automatiquement.

Quelques exemples :

  • __name__ : contient le nom de la Classe. Comme tout est objet en Python, cette variable va contenir "__main__" s'il s'agit du fichier comportant le code Python que vous venez de lancer. C'est un objet aussi à l'intérieur de Python.
  • __doc__ : contient la documentation de la Classe; si elle existe
  • __dict__ : contient le dictionnaire des attributs. Les clés sont les noms des attributs et les valeusr le contenu des attributs.

Pourquoi le double underscore ne crée pas vraiment d'attribut privé ?

Pour rappel, nommer un attribut ou une méthode en le faisant commencer par deux underscores permet de refuser son accès direct. C'est donc une sorte de grandeur privée, non ?

C'est vrai, mais c'est faux aussi :o)

Un exemple permettant de comprendre pourquoi c'est un peu vrai :

1 2 3 4 5 6 7
class Invisible : def __init__(self, a) : self.__visible = a self.visible = a*2 def affiche_toi(self) : print(self.__visible)

Voyons pourquoi l'attribut __visible peut être vu comme privé :

>>> toto = Invisible(50) >>> toto.visible 100 >>> toto.__invisible AttributeError: 'Invisible' object has no attribute '__invisible' >>> toto.affiche_toi() 50

Comme on peut le voir, on parvient à accéder à l'attribut depuis une méthode de la Classe mais pas directement depuis l'extérieur. On peut donc considérer qu'elle est privée.

C'est vrai. Sauf si on sait qu'il existe un dictionnaire qui regroupe les noms et les valeurs des attributs des objets.

Il s'agit de l'attribut spécial __dict__.

>>> toto = Invisible(50) >>> toto <__main__.Invisible object at 0x7fb7d6541400> >>> toto.__dict__ {'_Invisible__visible': 50, 'visible': 100} >>> toto.__dict__['visible'] 100 >>> toto.__dict__['_Invisible__visible'] 50

Comme vous pouvez le voir : on parvient au final à accéder au contenu sans trop de problème.

D'ailleurs, on peut même modifier cet attribut :

>>> toto.__dict__['_Invisible__visible'] = 200 >>> toto.__dict__['_Invisible__visible'] 200 >>> toto.affiche_toi() 200

Pourquoi le double underscore ne crée pas vraiment de méthode privée ?

Voici une méthode "privée".

1 2 3 4
class Invisible : def __pasmoyen(self) : print("Je suis totalement inaccessible ! Ah ah ah !")
>>> a = Invisible() >>> a.__pasmoyen() AttributeError: 'Invisible' object has no attribute '__pasmoyen'

Visiblement, c'est clair : on ne peut pas y accéder...

Allez : la solution.

>>> a._Invisible__pasmoyen() Je suis totalement inaccessible ! Ah ah ah !

Et voilà. Je l'avais dit. Pas de méthodes vraiment privées en Python.

C'est pour cela qu'un simple underscore suffit : si quelqu'un veut vraiment passer outre vos recommandations, il pourra de toutes manières...

En Python, c'est en mode interactif.

C'est presque pareil dans les autres langages : on peut vraiment créer des attributs et méthodes cachés mais on ne peut pas empêcher quelqu'un de modifier le code source si vous lui en donnez l'accès.

Activité publiée le 07 09 2020
Dernière modification : 22 09 2020
Auteur : ows. h.