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

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" :
- il faut tabuler pour que l'interpréteur Python comprenne l'appartenance de la méthode à la Classe.
- 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 |
|
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 |
|
- L'argument heros est stocké dans le paramètre self
- L'argument 5 est stocké dans le paramètre x
Regardons la méthode dans son intégralité :
14
15
16 |
|
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

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é :

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 |
|
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 |
|
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.
- Les mutateurs
- Les accesseurs
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.
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 |
|
...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 |
|
...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 |
|
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 :
- Possible avec Python mais pas légitime du point de vue de l'encapsulation
- Possible et légitime si on tente de respecter le principe d'encapsulation qu'on vient de voir
- 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
- Instruction 1 : on tape cette instruction dans la console (l'instruction se situe donc à l'extérieur de l'objet) :
- Instruction 2 : on place cela dans l'une des méthodes (l'instruction se situe donc à l'intérieur de l'objet) :
>>> heros.pv = 50
self.niveau = 50
...CORRECTION...
- Instruction 1 : on tape cette instruction dans la console :
- Instruction 2 : on place cela dans l'une des méthodes
>>> 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.
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.
- Un attribut attaque qui va servir à savoir si le personnage parvient à porter un coup à un autre personnage.
- Un attribut esquive qui va servir à savoir si le personnage parvient à esquiver le coup qu'un autre personnage lui porte.
- Un attribut puissance qui va correspondre aux nombres de dégats effectifs si le coup est bien porté.
- 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 |
|
✎ 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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.
- 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".
Par exemple, on pourrait dire que :
✎ 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 ?
- Ca ne rend pas vraiment pas les variables privées (voir FAQ pour si vous voulez savoir pourquoi)
- 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 |
|
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 |
|
>>> 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.