python méthodes

Identification

Infoforall

37 - Les méthodes dans les objets


Nous avons vu qu'un objet est une sorte de conteneur permettant d'accéder à un espace des noms qui permet lui-même d'accéder à  :

  • des attributs
  • des 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-15-16 + option : 17-18

Documents de cours : open document ou pdf

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

1 - Déclaration des méthodes

1.1 Déclaration d'une méthode

Déclarer une méthode revient à déclarer une fonction à l'intérieur de la déclaration de la Classe.

Deux différences avec une fonction "classique" :

  1. il faut tabuler pour que l'interpréteur Python comprenne l'appartenance de la méthode à la Classe.
  2. la méthode doit nécessairement avoir un premier paramètre qui recevra automatiquement l'adresse de l'objet sur lequel on est en train d'agir. En Python, on le nomme souvent self.
  3. 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, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession self.pv_max = self.niveau * 10 self.pv = self.pv_max def soigner(self, x): '''Le personnage regagne x points de vie''' self.pv = self.pv + x # Programme principal heros = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit")
Documentation rapide des types

Attention, on ne peut pas donner le nom de la Classe directement dans la documentation rapide des types des paramètres.

Le plus simple est de simplement fournir un string.

def soigner(self:'Personnage', x:int) -> None:

1.2 Comprendre l'appel à une méthode

Une explication détaillée

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

>>>  heros .soigner( 5 )

La sémantique est "Soigne le héros de 5 points".

Pour comprendre comment cela fonctionne, il faut transformer mentalement l'appel :

>>> Personnage.soigner( heros ,  5 )

On comprend qu'on demande d'appeller la méthode soigner() de la classe Personnage en envoyant l'adresse de l'objet heros et l'entier 5.

Si on regarde la déclaration ligne 14, on comprend mieux quel paramètre reçoit quel argument :

14
def soigner( self ,  x ):
  1. L'argument  heros  est stocké dans le paramètre  self 
  2. L'argument  5  est stocké dans le paramètre  x 

Regardons la méthode dans son intégralité :

14 15 16
def soigner(self, x): '''Le personnage regagne x points de vie''' self.pv = self.pv + x

On peut donc maintenant facilement comprendre ce que fait cette méthode : la ligne 16 montre qu'on incrémente l'attribut pv de x pour l'objet sur lequel on a lancé la méthode. On ne sait pas qui il est mais on connaît son adresse car elle a été stockée dans self.

Conclusion : ce qu'il faut savoir faire

Une fois qu'on a compris comment cela fonctionne, on peut se passer de cette étape : lors d'un appel, self contient l'adresse de l'objet qui se trouve devant le point.

Exemple :

>>> heros.soigner(5)

Le paramètre self va contenir heros lors de cet appel.

1.3 Modification via méthode vs modification directe : interfaçage le retour !

Pourquoi créer une méthode soigner() qui va modifier notre attribut si on peut faire pareil avec une instruction ?

>>> heros.soigner(5)

VS

>>> heros.pv = heros.pv + 5

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à conçu une pédale qui fait ce que vous voulez de façon sécurisée ?

Conclusion : le principe de l'interface via l'utilisation des méthodes permet à l'utilisateur de provoquer l'effet voulu sans risquer d'endommager les données internes de l'objet, sans risque de manipulation directe mal effectuée.

  • Une voiture offre une interface composée du volant, des pédales, des boutons... : .
  • Un module offre une interface composée de ses fonctions publiques.
  • Un système d'exploitation offre une interface composée d'appels système.
  • Un type abstrait offre une interface composée de primitives.
  • Un objet offre une interface composée de méthodes publiques.
1.4 Encapsulation

Cette notion d'interface va même souvent plus loin en programmation orientée objet réelle.

Principe de l'encapsulation

Seule une instance d'une classe A peut modifier directement les attributs d'une instance d'une Classe A sans passer par une méthode.

Une instance d'une classe B doit utiliser une méthode de la classe A s'il veut agir sur une instance de la classe A.

Si on veut modifier un objet, on doit lui demander poliment.

Cette notion se nomme l'encapsulation et il s'agit de l'un des principes fondamentaux de la programmation orientée objet (POO) :

  • on fournit à l'utilisateur une Classe qui contient un ensemble de méthodes d'interface
  • niveau d'encapsulation 1 : l'utilisateur n'est pas autorisé à modifier directement les attributs d'un objet.
  • niveau d'encapsulation 2 : l'utilisateur n'est pas autorisé à lire les attributs sans passer par l'interface.
  • niveau d'encapsulation 3 : l'utilisateur n'est pas autorisé à connaître les noms des attributs.

Analogie avec la voiture

interaction entre objet et utilisateur

L'utilisateur ne voit que cela de la voiture : les méthodes 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

Respect de l'encapsulation ou pas ?

Comme vous pouvez le voir, le concepteur voit plus de choses et peut agir sur plus de choses qu'un simple utilisateur. Il y a :

  • Les méthodes publiques : n'importe qui peut les utiliser.
  • Les méthodes privées et les attributs internes de l'objet : on ne doit en faire l'appel qu'à l'intérieur de la Classe, depuis une méthode.

Cela veut dire qu'on ne devrait même pas voir de choses de ce type :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# 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, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession self.pv_max = self.niveau * 10 self.pv = self.pv_max # Programme principal heros = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit") heros.pv = heros.pv + 10 etat = heros.pv

Les lignes 18 et 19 posent problème car l'utilisateur accède directement à l'un des attributs de l'objet. Cela ne respecte pas le principe d'encapsulation.

AVANTAGE : cela crée des programmes très courts.

DESAVANTAGE : cela ne respecte pas le principe d'encapsulation et peut provoquer des problèmes de cohérence interne de l'objet. A partir de la ligne 18, le personnage a plus de pv que les pv max par exemple.

Pour règler le problème, il faudra créer une méthode pour modifier les pv et une méthode qui lit cet attribut et renvoie la valeur. Ou encore mieux : la méthode renvoie juste un pourcentage lié à l'état du personnage :

  • à 0, on sait que le personnage n'a plus de point de vie,
  • à 100 on sait qu'il est en pleine forme.

De cette façon, l'utilisateur ne sera pas tenté de manipuler l'intérieur de notre belle classe avec ses petits doigts.

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
# 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, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession self.pv_max = self.niveau * 10 self.pv = self.pv_max def soigner(self, x): '''Le personnage regagne x points de vie''' self.pv = self.pv + x def obtenir_etat(self): '''Renvoie le pourcentage de forme du personnage, 0 si HS et 100 si pleine forme''' return int(self.pv / self.pv_max * 100) # Programme principal heros = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit") heros.soigner(10) etat = heros.obtenir_etat()

Cette fois, c'est mieux : l'utilisateur obtient une valeur en utilisant une méthode et n'a aucune idée en plus de la vraie valeur utilisée en interne.

AVANTAGE : on respecte le principe d'encapsulation.

DESAVANTAGE : ca demande de créer beaucoup de méthodes d'accès.

Remarque finale

Tout ce qu'on vient de dire concerne bien un utilisateur qui veut utiliser un objet depuis l'extérieur. Vous avez bien entendu le droit de manipuler directement les attributs si vos instructions sont situées à l'intérieur d'une méthode de cette Classe. Comme dans la méthode soigner() par exemple.

VOCABULAIRE HORS PROGRAMME : accesseur et mutateur

Remarque préalable

L'encapsulation n'est pas au programme de NSI.

Dans les sujets du BAC, on peut donc lire et modifier directement les objet depuis l'extérieur. Cela permet d'obtenir des programmes avec moins de lignes car avec moins de méthodes d'interface.

Attention : c'est une mauvaise pratique (notamment en projet).

Complément de vocabulaire

L'encapsulation impose de ne pouvoir interagir avec l'objet qu'à travers des méthodes. Il existe deux grandes familles de méthodes.

  1. Les mutateurs
  2. Ce sont les méthodes permettant de modifier les attributs de l'objet. On leur fait la demande, elles vérifient si la demande est valide et agissent en conséquence.

    Elles portent souvent des noms comme modifier_xxx(), regler_xxx(), configurer_xxx()... En anglais, on les fait souvent commencer par set_xxx().

    Ici, soigner() peut être qualifiée de mutateur.

  3. Les accesseurs
  4. Ce sont les méthodes permettant de lire les attributs de l'objet, ou au moins de récupérer une valeur liée à cette valeur interne. On leur fait la demande, elles lisent les attributs et fournissent une réponse.

    Elles portent souvent des noms comme lire_xxx(), obtenir_xxx(), recuperer_xxx()... En anglais, on les fait souvent commencer par get_xxx().

    Ici, obtenir_etat() peut être qualifiée d'accesseur.

2 - Exercices

01° Analyser le programme suivant.

Répondre aux questions suivantes, d'abord sans lancer le programme :

  • Combien de points de vie (attribut pv) aura le personnage heros de la ligne 20 ?
  • 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, profession="Humain de base"): self.nom = nom self.prenom = prenom self.niveau = niveau self.profession = profession self.pv_max = self.niveau * 10 self.pv = self.pv_max def mystere(self, degats): '''Mystère mystère''' self.pv = self.pv - degats # Instructions du programme principal heros = Personnage(nom="Snow", prenom="John", niveau=20, profession="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*20, soit 200 (voir ligne 11).

    L'attribut pv est alors également initialisé à cette valeur de 200 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 200 >>> heros.mystere(5) >>> heros.pv 195

03° Cette instruction est-elle similaire à l'appel heros.mystere(5) ?

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

...CORRECTION...

Non : il se passe exactement la même chose (on diminue l'attribut pv de 5 points). Mais avec la méthode, on respecte le principe d'encapsulation (on formule la demande à l'objet) alors qu'ici, on manipule directement l'objet.

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.
  • si ce nombre de pv (points de vie) devient négatif, on le ramène à 0.

On vous fournit un exemple dans la documentation. Le module doctest permettra donc de tester les exemples fournis dans la 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 57 58
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, profession="Humain de base"): ''' :: param nom(str) :: le nom du personnage :: param prenom(str) :: 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.profession = profession # Attributs calculés à partir des autres paramètres self.pv_max = self.niveau * 10 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 .. POSTCONDITION :: 0 ≤ self.pv ≤ self.pv_max .. exemples .. >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 100 >>> heros_test.modifier_pv(50) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 95 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 85 >>> 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=20, profession="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, profession="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.profession = profession # Attributs calculés à partir des autres paramètres self.pv_max = self.niveau * 10 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 100 >>> heros_test.modifier_pv(50) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 95 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 85 >>> 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 # Instructions du programme principal if __name__ == '__main__': import doctest doctest.testmod() heros = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit")

05° Mettre le programme de la correction précédente en mémoire 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.

>>> heros.pv 200 >>> heros.modifier_pv(-10) >>> heros.pv 190 >>> heros.modifier_pv(300) >>> heros.pv 200 >>> heros.modifier_pv(-300) >>> heros.pv 0

06° Dire si chacune des instructions suivantes est :

  1. Possible avec Python mais pas légitime du point de vue de l'encapsulation
  2. Possible et légitime si on tente de respecter le principe d'encapsulation qu'on vient de voir
  3. Impossible et renvoie une erreur

On considère que la classe Personnage 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 la console (l'instruction se situe donc à l'extérieur de l'objet) :
  2. >>> heros.pv = 50
  3. Instruction 2 : on place cela dans l'une des méthodes (l'instruction se situe donc à l'intérieur de l'objet) :
  4. self.niveau = 50

...CORRECTION...

  1. Instruction 1 : on tape cette instruction dans la console :
  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 et légitime en POO : on modifie les attributs de l'objet depuis l'intérieur de son propre code.

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 mais si le personnage évolue, il va falloir les recalculer. On décide donc de créer une méthode mise_a_jour() qui va contenir l'ensemble des calculs et modifications à effectuer.

Voici le programme 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
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, profession="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.profession = profession # 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 attributs''' self.pv_max = self.niveau * 10 if self.profession == '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 :: 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 self par effet de bord .. exemples .. >>> heros_test = Personnage(niveau=10) >>> heros_test.pv 100 >>> heros_test.modifier_pv(50) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 95 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 85 >>> 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 def subit_des_degats(self, degats): '''Active modifier_pv de façon à provoquer des dégats''' pass def parvient_a_blesser(self, cible): '''Le personnage lance une attaque contre la cible. Renvoie True si la cible subit des dégâts, False sinon :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (bool) :: True si cible touchée ''' pass def obtenir_etat(self): '''Renvoie un string décrivant l'état du personnage''' return '' # Instructions du programme principal if __name__ == '__main__': import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, profession="Jedi")

✎ 07° Expliquer pourquoi on visualise les valeurs suivantes dans la console.

>>> heros1.pv 200 >>> heros1.protection 10 >>> heros2.pv 80 >>> heros2.protection 8

✎ 08° Expliquer pourquoi on doit placer self suivi d'un point sur la ligne 27 pour activer la méthode mise_a_jour() sur l'objet en cours de traitement.

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
    • 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 profession (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 à une épée laser, ça risque de ne pas durer longtemps.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
def mise_a_jour(self): '''Calcule les valeurs des différents attributs''' self.pv_max = self.niveau * 10 if self.profession == 'Jedi': self.attaque = self.niveau self.esquive = self.niveau self.puissance = self.niveau self.protection = self.niveau elif self.profession == 'Rebelle': self.attaque = self.niveau self.esquive = self.niveau // 2 self.puissance = self.niveau self.protection = self.niveau // 2 elif self.profession == 'Stormtrooper': self.attaque = self.niveau // 2 self.esquive = self.niveau // 2 self.puissance = self.niveau // 2 self.protection = self.niveau // 2 else: self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3

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. Cela reviendrait à guérir !

Il faudra également finaliser la documentation, très incomplète pour le moment.

Vous pourrez tester de cette façon dans la console :

>>> heros1.pv 200 >>> heros1.subit_des_degats(10) >>> heros1.pv 190 >>> heros1.subit_des_degats(-20) >>> heros1.pv 190

...CORRECTION...

1 2 3 4
def subit_des_degats(self, degats): '''Active modifier_pv de façon à provoquer des dégats''' if degats > 0: self.modifier_pv(-degats)

11° Rajouter la documentation de la méthode précédente (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.

...CORRECTION...

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
def subit_des_degats(self, degats): '''Active modifier_pv de façon à provoquer des dégats :: param self(Personnage) :: l'instance sur laquelle on agit :: param degats(int) :: les dégats qu'on veut infliger, sans précondition de signe :: return None :: "procédure" .. effet de bord :: modifie self par effet de bord .. exemples .. >>> h = Personnage(niveau=20) >>> h.pv 200 >>> h.subit_des_degats(10) >>> h.pv 190 >>> h.subit_des_degats(-20) >>> h.pv 190 ''' if degats > 0: self.modifier_pv(-degats)

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

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

>>> heros1.parvient_a_blesser(heros2)

Voici le prototype de la méthode :

1
def parvient_a_blesser(self:'Personnage', cible:'Personnage') -> bool:

12° Question théorique : que contient le paramètre self lors de cet appel ? Que contient le paramètre cible ?

>>> heros1.parvient_a_blesser(heros2)

...CORRECTION...

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

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

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

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

C'est comme si nous avions tapé ceci :

>>> Personnage.parvient_a_blesser(heros1, 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].

1 2
import random d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20]

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

  • Si  (attaque de l'attaquant + 1d20)  est supérieur à  (esquive du défenseur + 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)  est supérieur à  esquive du défenseur  :
    • on diminue les pv du défenseur de  (10 + puissance de l'attaquant - protection du défenseur) 
  • Sinon :
    • on ne fait rien, l'attaque rate

On notera que les dégâts pourraient être négatifs dans certains cas. C'est pour cela qu'il est important de ne gérer que les valeurs positives de dégâts.

13° Rajouter l'importation du module en début de programme puis compléter la méthode parvient_a_blesser() 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 parvient_a_blesser() ne fait rien, à part tirer un nombre aléatoire entre 1 et 20 et placer le résultat dans une variable.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import random class Personnage: '''Ceci est une classe permettant de créer un personnage dans mon super RPG''' ... ... ... def parvient_a_blesser(self, cible): '''Le personnage lance une attaque contre la cible. Renvoie True si la cible subit des dégâts, False sinon :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (bool) :: True si cible touchée ''' d20 = random.randint(1, 20) # d20 contient alors une valeur entière dans [1,20]

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.parvient_a_blesser(heros2) False >>> heros2.parvient_a_blesser(heros1) True >>> heros1.pv 167

...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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
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, profession="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.profession = profession # 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 attributs''' self.pv_max = self.niveau * 10 if self.profession == 'Jedi': self.attaque = self.niveau self.esquive = self.niveau self.puissance = self.niveau self.protection = self.niveau elif self.profession == 'Rebelle': self.attaque = self.niveau self.esquive = self.niveau // 2 self.puissance = self.niveau self.protection = self.niveau // 2 elif self.profession == 'Stormtrooper': self.attaque = self.niveau // 2 self.esquive = self.niveau // 2 self.puissance = self.niveau // 2 self.protection = self.niveau // 2 else: self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 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 100 >>> heros_test.modifier_pv(50) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 95 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 85 >>> 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 def subit_des_degats(self, degats): '''Active modifier_pv de façon à provoquer des dégats :: param self(Personnage) :: l'instance sur laquelle on agit :: param degats(int) :: les dégats qu'on veut infliger, sans précondition de signe :: return None :: "procédure" .. effet de bord :: modifie self par effet de bord .. exemples .. >>> h = Personnage(niveau=20) >>> h.pv 200 >>> h.subit_des_degats(10) >>> h.pv 190 >>> h.subit_des_degats(-20) >>> h.pv 190 ''' if degats > 0: self.modifier_pv(-degats) def parvient_a_blesser(self, cible): '''Le personnage lance une attaque contre la cible. Renvoie True si la cible subit des dégâts, False sinon :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (bool) :: True si cible touchée ''' degats = 0 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) return degats > 0 def obtenir_etat(self): '''Renvoie un string décrivant l'état du personnage''' return '' # Instructions du programme principal if __name__ == '__main__': import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, profession="Jedi")

Nous avons déjà vu des mutateurs dans les questions précédentes. Ce qui nous manque, ce sont des exemples d'accesseurs. Pour l'instant, le seul moyen de connaître le nombre de PV est d'interroger directement l'objet :

>>> heros2.pv 80

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

14° Reprendre si besoin la correction de la classe fournie sur la question précédente. Compléter la méthode obtenir_etat() pour qu'elle renvoie un string correspondant à la valeur de l'attribut pv de l'objet.

...CORRECTION...

1 2 3 4 5
def obtenir_etat(self): '''Renvoie un string décrivant l'état du personnage''' return str(self.pv)

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.
  • Ensuite, on peut modifier la valeur : pas besoin de dire la vérité. On pourrait par exemple renvoyer un pourcentage des pv restants plutôt que la valeur brute.
  • Par exemple, on pourrait dire que :

    • De 0 à 25 % des points de vie : le personnage est "Blessé".
    • De 25 à 75 % des points de vie : le personnage est "Fatigué".
    • A plus de 75%, le personnage est "En forme".

✎ 15° On veut maintenant que notre méthode obtenir_etat() renvoie un string informatif plutôt que la valeur des points de vie :

  • "HS" si les PV sont à 0
  • "Pleine forme" si les PV dépassent 75% du maximum possible
  • "Blessé" si les PV sont inférieurs à 25% du maximum possible
  • Sinon "Fatigué"

De cette façon, le joueur n'a plus aucune connaissance de la valeur exacte de ses points de vie. Il obtient juste une indication à travers la méthode accesseur.

✎ 16° Un élève présente cette séquence et affirme que c'est équivalent au travail que vous avez fourni sur la question précédente. Il se trompe. Sa version n'est pas valide car elle ne fonctionne pas toujours. Mais pourquoi ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
def obtenir_etat(self): '''Renvoie un string décrivant l'état du personnage''' p = self.pv / self.pv_max if self.pv == 0: reponse = "HS" elif p > 0.25: reponse = "Fatigué" elif p > 0.75: reponse = "En forme" else: reponse = "Blessé" return reponse
Rappel : tester le type d'un objet
>>> type(heros1) <class '__main__.Personnage'>

Pour rappel, voilà comment tester le type d'une variable faisant référence (ou pas) à un objet.

>>> type(heros1) == Personnage True >>> isinstance(heros1, Personnage) True

Dans le cadre de la NSI, il est préférable d'utiliser simplement la fonction native type() puisque vous la connaissez déjà. isinstance() fait exactement la même chose en l'état actuel de vos connaissances.

3 - Programme de l'arène

Maintenant que nous avons notre Classe, nous allons pouvoir l'utiliser pour réaliser des programmes les utilisant. Par exemple, nous pourrions réaliser un programme où on désigne deux combattants et qui décrit le combat.

17° Enregistrer le programme sous le nom mes_classes.py de façon à en faire un module.

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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
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, profession="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.profession = profession # 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 attributs''' self.pv_max = self.niveau * 10 if self.profession == 'Jedi': self.attaque = self.niveau self.esquive = self.niveau self.puissance = self.niveau self.protection = self.niveau elif self.profession == 'Rebelle': self.attaque = self.niveau self.esquive = self.niveau // 2 self.puissance = self.niveau self.protection = self.niveau // 2 elif self.profession == 'Stormtrooper': self.attaque = self.niveau // 2 self.esquive = self.niveau // 2 self.puissance = self.niveau // 2 self.protection = self.niveau // 2 else: self.attaque = self.niveau // 3 self.esquive = self.niveau // 3 self.puissance = self.niveau // 3 self.protection = self.niveau // 3 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 100 >>> heros_test.modifier_pv(50) >>> heros_test.pv 100 >>> heros_test.modifier_pv(-5) >>> heros_test.pv 95 >>> heros_test.modifier_pv(-10) >>> heros_test.pv 85 >>> 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 def subit_des_degats(self, degats): '''Active modifier_pv de façon à provoquer des dégats :: param self(Personnage) :: l'instance sur laquelle on agit :: param degats(int) :: les dégats qu'on veut infliger, sans précondition de signe :: return None :: "procédure" .. effet de bord :: modifie self par effet de bord .. exemples .. >>> h = Personnage(niveau=20) >>> h.pv 200 >>> h.subit_des_degats(10) >>> h.pv 190 >>> h.subit_des_degats(-20) >>> h.pv 190 ''' if degats > 0: self.modifier_pv(-degats) def parvient_a_blesser(self, cible): '''Le personnage lance une attaque contre la cible. Renvoie True si la cible subit des dégâts, False sinon :: param self(Personnage) :: l'instance qui attaque :: param cible(Personnage) :: l'instance qui se défend, différente de la première ! :: return (bool) :: True si cible touchée ''' degats = 0 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) return degats > 0 def obtenir_etat(self): '''Renvoie un string décrivant l'état du personnage''' p = self.pv / self.pv_max if self.pv == 0: reponse = "HS" elif p > 0.75: reponse = "En forme" elif p > 0.25: reponse = "Fatigué" else: reponse = "Blessé" return reponse def obtenir_nom(self): return self.nom # Instructions du programme principal if __name__ == '__main__': import doctest doctest.testmod() heros1 = Personnage(nom="Snow", prenom="John", niveau=20, profession="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, profession="Jedi")

18° Enregistrer le programme suivant dans le même répertoire que le module précédent.

Lancer et expliquer le fonctionnement de ce programme.

Si la moindre ligne pose problème, pensez à faire appel à l'enseignant.

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
from mes_classes import Personnage def decrire_debut(c1, c2): print(f"Le combat commence entre {c1.obtenir_nom()} et {c2.obtenir_nom()}") def decrire_attaque(a, b): print(f"{a.obtenir_nom()} parvient à toucher {b.obtenir_nom()} dont l'état est maintenant {b.obtenir_etat()}") def decrire_conclusion(a): print(f"\n{a.obtenir_nom()} a gagné le duel et est maintenant {a.obtenir_etat()}.") def duel(adv1, adv2): '''Décrit le duel entre adv1 et adv2. Attention, cela modifie les combattants.''' decrire_debut(adv1, adv2) attaquant = adv1 defenseur = adv2 while adv1.obtenir_etat() != "HS" and adv2.obtenir_etat() != "HS": if attaquant.parvient_a_blesser(defenseur): decrire_attaque(attaquant, defenseur) temp = attaquant attaquant = defenseur defenseur = temp if adv1.obtenir_etat() == "HS": gagnant = adv2 else: gagnant = adv1 decrire_conclusion(gagnant) heros1 = Personnage(nom="Snow", prenom="John", niveau=15, profession="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, profession="Jedi") duel(heros1, heros2)

Le programme précédent respecte l'encapsulation et la distinction entre fonction d'interface et gestion des données. Voici un programme qui fait la même chose mais sans respect réel de l'encapsulation, ni de la distinction entre interface et données.

19° Enregistrer le nouveau programme dans le même répertoire que le module précédent.

Lancer et vérifier qu'il provoque le même comportement externe que le programme de la question précédente.

Avantage par rapport à la version précédente ?

Désavantage par rapport à la version précédente ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
from mes_classes import Personnage def duel(adv1, adv2): '''Décrit le duel entre adv1 et adv2. Attention, cela modifie les combattants.''' print(f"Le combat commence entre {adv1.nom} et {adv2.nom}") attaquant = adv1 defenseur = adv2 while adv1.pv > 0 and adv2.pv > 0: if attaquant.parvient_a_blesser(defenseur): print(f"{attaquant.nom} parvient à toucher {defenseur.nom} dont l'état est maintenant {defenseur.obtenir_etat()}") temp = attaquant attaquant = defenseur defenseur = temp if adv1.pv == 0: print(f"\n{adv2.nom} a gagné le duel et est maintenant {adv2.obtenir_etat()}.") else: print(f"\n{adv1.nom} a gagné le duel et est maintenant {adv1.obtenir_etat()}.") heros1 = Personnage(nom="Snow", prenom="John", niveau=15, profession="Garde de Nuit") heros2 = Personnage(nom="Skywalker", prenom="Luke", niveau=8, profession="Jedi") duel(heros1, heros2)

4 - Partie optionnelle : autres méthodes spéciales

Aucune connaissance exigible à ce sujet pour le BAC. Seule __init__() est clairement à connaître.

Par contre, savoir qu'elles existent peu être bien pratiques lors des projets.

Nous allons voir ici deux nouvelles méthodes spéciales.

Méthode _add__()

La méthode _add__() permet de définir ce qu'on doit faire lorsqu'on tente de faire instance + ?.

Imaginons qu'on veuille créer la signature suivante :

Personnage + Personnage -> bool

La sémantique correspondrait au fait que le premier personnage tente d'attaquer le second.

Il suffit alors de rajouter les lignes dans la Classe :

1 2 3 4 5 6 7
class Personnage: '''Ceci est une classe permettant de créer un personnage dans mon super RPG''' ... def __add__(self, cible): return self.parvient_a_blesser(cible)

Une fois en mémoire, on pourra alors utiliser l'opérateur + pour signaler qu'un personnage veut en attaquer un autre.

>>> heros1 + heros2 True

Comme vous le voyez, cette méthode est spéciale car on ne l'active pas à l'appellant directement : c'est l'interpréteur Python qui active __add__() lorsqu'on veut utiliser + avec une instance de Personnage.

Méthode _str__()

Avec cette méthode spéciale, nous allons pouvoir donner l'affichage qu'on désire obtenir lorsqu'on utilise la fonction native str().

1 2 3 4 5 6 7
class Personnage: '''Ceci est une classe permettant de créer un personnage dans mon super RPG''' ... def __str__(self): return f"{self.prenom} {self.nom} est {self.profession} niveau {self.niveau}"

Une fois en mémoire, on pourra alors utiliser la fonction native str() puisqu'on vient d'expliciter le string à renvoyer dans ce cas..

>>> str(heros1) 'John Snow est Garde de Nuit niveau 20'

Comme vous le voyez, cette méthode est spéciale car on ne l'active pas à l'appellant directement : c'est l'interpréteur Python qui active __str__() lorsqu'on veut utiliser str() avec une instance de Personnage.

D'ailleurs, que fait l'interpréteur Python lorsqu'on utilise print() ? Il demande d'abord à str() de lui fournir la représentation string de l'argument qu'il a reçu. En conséquence, si on fournit un Personnage à la fonction native print(), elle va faire appel à str() qui va faire appel à __str__() si on l'a configuré.

>>> heros1 <__main__.Personnage object at 0x7ff00c5b7828> >>> print(heros1) John Snow est Garde de Nuit niveau 20

20° Rajouter ces deux méthodes dans votre classe Personnage. Plus qu'à tester pour voir si cela fonctionne bien comme prévu. Normalement oui.

5 - Partie très très optionelle : Privé-Public avec Python

Une partie à ne faire que si vous avez vraiment le temps en étant vraiment très en avance. Ce n'est pas au programme puisqu'on rentre dans des détails de Python.

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

21 (optionnelle)° Utiliser les instructions suivantes dans la console pourvoir 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 (voir FAQ pour si vous voulez savoir pourquoi)
  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.

22 (optionnelle)° Appliquer l'attribut spécial __dict__ à une instance de Personnage.

>>> 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}

C'est notamment à cause de cet attribut spécial qu'il n'est pas possible de vraiment obtenir des attributs privés dans Python : on peut toujours trouver le nom d'un attribut et tenter d'y accéder grace à ce dictionnaire. Voir FAQ si vous voulez des détails.

6 - FAQ

J'ai lu qu'on parle de variables-membres. Qu'est-ce que c'est ?

Le vocabulaire de la POO est vaste. Cette année, on vous demande de savoir définir classe, objet, attribut et méthode. C'est déjà pas mal.

Mais il existe d'autres mots de vocabulaire qui sont parfois des synonymes, parfois des généralisations.

Pour votre culture générale :

  • Membre désigne l'ensemble des attributs et méthodes contenus dans une classe et ses instances.
  • Variable-membre est donc un synonyme d'attribut.
  • Fonction-membre est donc un synonyme de méthode.
  • les méthodes accesseurs sont les méthodes permettant à l'utilisateur d'accéder à certaines valeurs : on pourrait ainsi, non pas voir les vrais pv d'un personnage, mais avoir simplement un compteur ROUGE - ORANGE - VERT en fonction de son état.
  • les méthodes mutateurs sont les méthodes permettant de modifier les attributs des objets **après validation et vérification** de la demande. Ainsi, on garantit l'intégrité de l'objet.
  • Les membres privés sont les attributs et les méthodes qu'un utilisateur n'a pas le droit d'utiliser. Ces membres vont partie de la mécanique interne, mécanique qu'il faut préserver et surveiller.
  • Les membres publics forment l'interface : l'utilisateur a le droit de les utiliser. Concrétement, il s'agit donc uniquement des méthodes d'interface. Lorsqu'on veut faire un bel objet, les attributs devraient tous être privés et ne devraient pas pouvoir être modifiés directement par l'utilisateur. Il devrait devoir utiilser un accesseur pour le lire et un mutateur pour le modifier.

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.__visible AttributeError: 'Invisible' object has no attribute '__visible' >>> 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...

Comment obtenir les noms des méthodes ?

>>> dir(Personnage) ['_Invisible__pasmoyen', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'obtenir_etat', 'mise_a_jour', 'modifier_pv', 'subit_des_degats', 'parvient_a_blesser']

Est-ce plus sécurisé dans les autres langages ? Cela en a l'apparence car on peut créer des attributs et des méthodes réellement privées. Mais au final ,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 : 01 09 2022
Auteur : ows. h.