Ce corrigé donne le code de chaque exercice, mais surtout **la façon d'y arriver**. Sous chaque question, vous trouverez des blocs à déplier :
- **Par où commencer** : comment réfléchir avant d'écrire la moindre ligne ;
- **Ce qui se passe à l'exécution** : le code suivi pas à pas sur un exemple.
Si les mots *classe*, *objet* ou *méthode* ne sont pas clairs, relisez d'abord la [fiche de révision POO](../FICHE_REVISION.md).
---
## Avant de commencer : la méthode
### Quatre mots de vocabulaire
| Mot | Ce que c'est | Dans le TP |
|:--|:--|:--|
| **Classe** | Le moule, le plan de fabrication | `Pokemon` |
| **Objet** (ou instance) | Une chose fabriquée avec le moule | `pikachu`, `salameche` |
| **Attribut** | Une information rangée dans l'objet | `self.pv`, `self.nom` |
| **Méthode** | Une fonction rangée dans la classe, qui agit sur l'objet | `attaquer`, `soigner` |
### Que veut dire `self` ?
`self` désigne **l'objet sur lequel on appelle la méthode**. Quand vous écrivez `pikachu.soigner(20)`, Python exécute la méthode `soigner` en remplaçant `self` par `pikachu`. Lire `self.pv`, c'est donc lire « les PV de **ce** Pokémon-là ».
Règle à retenir : **dans une méthode, chaque attribut s'écrit avec `self.` devant**, sans exception.
### Par où commencer une méthode ?
Devant une méthode vide, ne cherchez pas le code tout de suite. Répondez dans l'ordre à ces trois questions, en français, sur votre brouillon :
1.**De quoi ai-je besoin ?** Regardez les paramètres (entre parenthèses) et les attributs de l'objet (`self.quelque_chose`).
2.**Que dois-je produire ?** Trois cas possibles, la docstring vous le dit :
- **renvoyer** une valeur (`:return:` dans la docstring), donc il faut un `return` ;
- **modifier** un attribut, donc il faut une ligne `self.attribut = ...` ;
- **afficher** un message, donc il faut un `print`.
3.**Quelles sont les étapes ?** Écrivez-les en français, une par ligne. Ensuite seulement, traduisez chaque étape en Python.
C'est exactement ce que font les blocs « Par où commencer » de ce corrigé.
### Les trois erreurs les plus fréquentes
!!! warning "Oublier `self`"
Trois formes différentes, trois messages d'erreur différents :
| Ce que vous avez écrit | Ce qu'il faut écrire | Message de Python |
|:--|:--|:--|
| `nom = nom` dans `__init__` | `self.nom = nom` | `AttributeError: 'Pokemon' object has no attribute 'nom'` (l'attribut n'a jamais été créé) |
| `return pv <= 0` | `return self.pv <= 0` | `NameError: name 'pv' is not defined` |
| `def est_ko():` | `def est_ko(self):` | `TypeError: Pokemon.est_ko() takes 0 positional arguments but 1 was given` |
Le dernier message est déroutant : vous n'avez passé aucun argument, et Python dit en avoir reçu un. Cet argument, c'est l'objet lui-même, que Python place automatiquement dans `self`.
!!! warning "Les virgules dans les f-strings"
Dans une f-string, **chaque paire d'accolades contient une seule valeur**. Tout ce qui est entre les accolades (espaces, parenthèses, tirets, virgules) est recopié tel quel.
| Ce que vous avez écrit | Ce qui s'affiche |
|:--|:--|
| `f"{self.nom, self.type_pokemon}"` | `('Pikachu', 'Electrik')` : la virgule dans l'accolade crée un tuple |
| `return f"{self.nom}", f"({self.type_pokemon})"` | `TypeError: __str__ returned non-string (type tuple)` : la virgule après le `return` renvoie deux chaînes au lieu d'une |
| `"{self.nom} ({self.type_pokemon})"` | `{self.nom} ({self.type_pokemon})` : sans le `f` devant, les accolades ne sont pas remplacées |
Méthode sûre : **écrivez d'abord le texte attendu**, par exemple `Pikachu (Electrik) - Nv.25 - 80/80 PV`, puis remplacez chaque valeur variable par une accolade : `{self.nom} ({self.type_pokemon}) - Nv.{self.niveau} - {self.pv}/{self.pv_max} PV`. Ajoutez le `f` et les guillemets.
!!! warning "Afficher au lieu de renvoyer"
`print` montre une valeur à l'écran, mais le programme ne peut plus s'en servir. `return` donne la valeur au programme. Quand la docstring contient `:return:`, il faut un `return`.
1.**De quoi ai-je besoin ?** Les paramètres de `__init__` : `nom`, `type_pokemon`, `niveau`, `pv_max`, `attaque`.
2.**Que dois-je produire ?**`__init__` ne renvoie rien : son rôle est de **ranger** chaque information dans l'objet, sous forme d'attribut.
3.**Les étapes :**
- pour chaque paramètre, créer l'attribut correspondant : `self.nom = nom`, etc. ;
- créer l'attribut `pv`, qui n'est pas un paramètre : un Pokémon neuf a tous ses PV, donc `self.pv = pv_max`.
Pour `__repr__` : la docstring demande une chaîne. Écrivez le texte attendu de l'énoncé, `Pikachu (Electrik) - Nv.25 - 80/80 PV`, puis remplacez chaque valeur par l'attribut correspondant entre accolades.
1. Python fabrique un objet vide, et le place dans `self`.
2. Il exécute `__init__` avec `nom = "Pikachu"`, `type_pokemon = "Electrik"`, `niveau = 25`, `pv_max = 80`, `attaque = 15`.
3. Chaque ligne `self.... = ...` remplit l'objet. À la fin, l'objet contient : `nom` vaut `"Pikachu"`, `type_pokemon` vaut `"Electrik"`, `niveau` vaut 25, `pv_max` vaut 80, `pv` vaut 80, `attaque` vaut 15.
4. L'objet est rangé dans la variable `pikachu`.
Ensuite, `print(pikachu)` appelle automatiquement `__repr__` : `{self.nom}` est remplacé par `Pikachu`, `{self.pv}` par `80`, etc. On obtient `Pikachu (Electrik) - Nv.25 - 80/80 PV`.
Ces méthodes s'ajoutent dans la classe `Pokemon`, avec la même indentation que `__repr__`.
??? tip "Par où commencer : `est_ko`"
1. **De quoi ai-je besoin ?** Aucun paramètre à part `self`. L'attribut utile est `self.pv`.
2. **Que dois-je produire ?** La docstring dit `:return: (bool)` : il faut **renvoyer** `True` ou `False`.
3. **Les étapes :** regarder si les PV sont inférieurs ou égaux à 0, et renvoyer la réponse.
`self.pv <= 0` est déjà une expression qui vaut `True` ou `False` : on peut la renvoyer directement.
??? tip "Par où commencer : `subir_degats`"
1. **De quoi ai-je besoin ?** Le paramètre `degats`, et l'attribut `self.pv`.
2. **Que dois-je produire ?** Pas de `:return:` dans la docstring. On **modifie** un attribut : `self.pv`.
3. **Les étapes :**
- enlever les dégâts aux PV ;
- si les PV sont passés sous 0, les remettre à 0.
??? tip "Par où commencer : `attaquer`"
1. **De quoi ai-je besoin ?** Le paramètre `cible`, qui est **un objet `Pokemon`** (la docstring le précise : `(Pokemon)`). On a donc accès à `cible.nom`, `cible.pv`, et à ses méthodes. On a aussi `self.attaque` et `self.nom`.
2. **Que dois-je produire ?** Un **affichage**, et une **modification** : les PV de la cible baissent.
3. **Les étapes :**
- afficher le message de l'énoncé : `Pikachu attaque Salamèche et inflige 15 dégâts !` ;
- faire subir les dégâts à la cible. Pas besoin de réécrire le calcul : la méthode `subir_degats` existe déjà, on l'appelle **sur la cible**.
??? tip "Par où commencer : `soigner`"
C'est le miroir de `subir_degats` :
- ajouter le soin aux PV ;
- si les PV dépassent `pv_max`, les ramener à `pv_max`.
1. On appelle `attaquer` **sur `pikachu`** : dans la méthode, `self` vaut `pikachu`, et `cible` vaut `salameche`.
2. Le `print` affiche `Pikachu attaque Salamèche et inflige 15 dégâts !` : `self.nom` donne `Pikachu`, `cible.nom` donne `Salamèche`, `self.attaque` donne 15.
3. La ligne `cible.subir_degats(self.attaque)` appelle `subir_degats` **sur `salameche`** avec `degats = 15`. Dans cette nouvelle méthode, `self` vaut maintenant `salameche`.
4. `self.pv = self.pv - degats` : les PV de Salamèche passent de 70 à 55. 55 n'est pas négatif, le `if` ne fait rien.
5. `salameche.est_ko()` calcule `55 <= 0`, ce qui vaut `False`.
Le point clé : `self` ne désigne pas toujours le même Pokémon. Il désigne celui **devant le point** au moment de l'appel.
!!! note "Une version plus courte"
`self.pv = max(0, self.pv - degats)` fait la même chose que les trois lignes de `subir_degats` : on garde le plus grand entre 0 et le résultat. De même, `self.pv = min(self.pv_max, self.pv + soin)` pour `soigner`. Les deux versions sont correctes.
1. **Comprendre le dictionnaire.** `FAIBLESSES["Eau"]` vaut `["Feu"]` : l'Eau est super efficace contre le Feu. La clé est le type de **l'attaquant**, la valeur est la liste des types **qu'il bat**.
2. **Que change cette version ?** Les dégâts ne valent plus toujours `self.attaque` : ils peuvent doubler. On a donc besoin d'une variable `degats`.
3. **Les étapes :**
- mettre `self.attaque` dans une variable `degats` ;
- si le type de l'attaquant est dans le dictionnaire, **et** si le type de la cible est dans sa liste, doubler `degats` et afficher `C'est super efficace !` ;
2. `self.type_pokemon in FAIBLESSES` : `"Eau"` est bien une clé du dictionnaire.
3. `FAIBLESSES["Eau"]` vaut `["Feu"]`, et `cible.type_pokemon` vaut `"Feu"` : il est dans la liste. `degats` passe à 44, et Python affiche `C'est super efficace !`.
4. Puis `Tortank attaque Dracaufeu et inflige 44 dégâts !`, et Dracaufeu passe de 150 à 106 PV.
Si un Onix (type `"Roche"`) attaquait, le premier `if` serait faux, car `"Roche"` n'est pas une clé : les dégâts resteraient normaux. Sans ce premier test, `FAIBLESSES["Roche"]` provoquerait une erreur `KeyError`.
!!! warning "Erreur fréquente"
Écrire `self.attaque = self.attaque * 2` au lieu de passer par `degats`. L'attaque du Pokémon serait doublée **pour toujours**, et redoublée à chaque coup super efficace.
Deux attributs : `nom` est donné en paramètre, mais `equipe` ne l'est pas. Un dresseur commence sans Pokémon : `self.equipe = []`.
??? tip "Par où commencer : `ajouter_pokemon`"
1.**De quoi ai-je besoin ?** Le paramètre `pokemon`, et l'attribut `self.equipe`.
2.**Que dois-je produire ?****Modifier** l'équipe si c'est possible, et **renvoyer**`True` ou `False` (`:return:`).
3.**Les étapes :**
- si l'équipe compte moins de 6 Pokémon, ajouter le Pokémon à la liste et renvoyer `True` ;
- sinon, renvoyer `False`.
??? tip "Par où commencer : `pokemon_disponibles`, `premier_pokemon_dispo`, `tous_ko`"
Ces trois méthodes parcourent l'équipe avec une boucle `for pokemon in self.equipe:`. À chaque tour, `pokemon` est un objet `Pokemon` : on peut donc lui demander `pokemon.est_ko()`.
-`pokemon_disponibles` : partir d'une liste vide, y ajouter chaque Pokémon qui n'est pas KO, renvoyer la liste.
-`premier_pokemon_dispo` : dès qu'on trouve un Pokémon qui n'est pas KO, le renvoyer. Si la boucle se termine sans rien trouver, renvoyer `None`.
-`tous_ko` : tous les Pokémon sont KO quand la liste des disponibles est vide. On réutilise la méthode précédente.
??? tip "Par où commencer : `__repr__`"
Le texte attendu tient sur plusieurs lignes :
```
Dresseur Sacha - 2 Pokémon
- Pikachu (Electrik) - Nv.25 - 80/80 PV
- Dracaufeu (Feu) - Nv.50 - 150/150 PV
```
- Construire la première ligne dans une variable `texte`.
- Pour chaque Pokémon de l'équipe, ajouter à `texte` un retour à la ligne (`\n`), deux espaces, un tiret, et le Pokémon.
Mettre le `return None` **dans** la boucle, au même niveau que le `if`. La méthode s'arrêterait alors dès le premier Pokémon KO, sans regarder les suivants. Le `return None` se place **après** la boucle, aligné avec le `for`.
Une bonne partie du code est déjà donnée. Avant de compléter, **jouez un tour à la main** sur votre brouillon : Pikachu (80 PV, attaque 15) contre Onix (120 PV, attaque 18).
- Pikachu attaque Onix. Onix est-il KO ? Non : il riposte.
- Onix attaque Pikachu. Pikachu est-il KO ? Non : on passe au tour suivant.
Traduisez ensuite chaque commentaire `# À compléter` :
1. **Le Pokémon 1 attaque** : une seule ligne, `poke1.attaquer(poke2)`.
2. **Vérifier si le Pokémon 2 est KO** : `if poke2.est_ko():`. Si oui, on l'annonce, et on annonce le remplaçant s'il y en a un.
3. **Le Pokémon 2 attaque (s'il n'est pas KO)** : c'est le `else` du test précédent. Un Pokémon KO ne riposte pas.
4. **Vérifier si le Pokémon 1 est KO** : même chose que l'étape 2, dans l'autre sens.
5. **Afficher le vainqueur** : après la boucle, l'un des deux dresseurs n'a plus de Pokémon. Si c'est le dresseur 1, le dresseur 2 a gagné, et inversement.
Pour `afficher_etat`, on récupère le Pokémon actif de chaque dresseur avec `premier_pokemon_dispo()`, puis on affiche son nom et ses PV.
Sacha (Pikachu, Dracaufeu, Tortank) contre Pierre (Onix, Racaillou).
**Tour 1.** Les Pokémon actifs sont Pikachu (80 PV) et Onix (120 PV).
- Pikachu attaque : Onix passe à 105 PV. Il n'est pas KO, donc on entre dans le `else`.
- Onix riposte : Pikachu passe à 62 PV. Il n'est pas KO.
- Fin du tour. La condition du `while` est toujours vraie : chaque dresseur a encore des Pokémon debout.
**Quelques tours plus tard**, Pikachu tombe à 0 PV pendant la riposte d'Onix. On affiche `Pikachu est KO !`, puis `premier_pokemon_dispo()` renvoie Dracaufeu : `Sacha envoie Dracaufeu !`.
**Au tour suivant**, on n'a rien eu à faire pour changer de Pokémon : `premier_pokemon_dispo()` saute Pikachu, qui est KO, et renvoie Dracaufeu.
**À la fin**, Racaillou tombe KO. `pierre.tous_ko()` vaut `True`, la condition du `while` devient fausse et la boucle s'arrête. Le `if` final affiche que Sacha remporte le combat.
!!! warning "Erreur fréquente"
Oublier le `else` et faire riposter le Pokémon 2 même s'il vient d'être mis KO. On voit alors un Pokémon à 0 PV attaquer, ce qui est le signe de cette erreur.
!!! note "Pourquoi `afficher_etat` ne vérifie pas `None`"
`afficher_etat` est appelée au début d'un tour, donc seulement quand la condition du `while` est vraie : chaque dresseur a forcément un Pokémon disponible. `premier_pokemon_dispo()` ne peut pas renvoyer `None` à ce moment-là.
Après le combat, Dracaufeu a 96 PV sur 150. `centre.soigner_equipe(sacha)` parcourt l'équipe de Sacha. Arrivé à Dracaufeu, il appelle `soigner(150)` : 96 + 150 = 246, ce qui dépasse 150, donc les PV sont ramenés à 150.
Remarquez la chaîne d'objets : le **centre** reçoit un **dresseur**, qui contient une **équipe**, qui contient des **Pokémon**, sur lesquels on appelle `soigner`.
On aurait aussi pu écrire `pokemon.pv = pokemon.pv_max`. Passer par la méthode `soigner` est plus propre : c'est le Pokémon qui gère ses propres PV.
L'énoncé est plus ouvert : c'est à vous de choisir les méthodes. Posez-vous la question « qu'est-ce qu'un Pokémon doit pouvoir **faire** avec ses capacités ? ».
- **Apprendre** une capacité : l'ajouter à une liste, 4 au maximum. C'est le même principe que `ajouter_pokemon` du dresseur.
- **Attaquer avec** une capacité : choisir laquelle, vérifier qu'il reste des PP, calculer les dégâts, les infliger.
Pour stocker les capacités, il faut un nouvel attribut dans `Pokemon` : une liste vide, créée dans `__init__`.
C'est une **composition** d'objets : un `Pokemon` possède une liste d'objets `Capacite`, comme un `Dresseur` possède une liste d'objets `Pokemon`. La formule de dégâts (`attaque + puissance`) est un choix parmi d'autres : toute règle cohérente et justifiée est acceptée.
Attaque avec ×2 dégâts si la cible est de type Eau.
"""
degats = self.attaque
if cible.type_pokemon == "Eau":
degats = degats * 2
print("C'est super efficace !")
print(f"{self.nom} attaque {cible.nom} et inflige {degats} dégâts !")
cible.subir_degats(degats)
```
**Explications :**
- `super().__init__(...)` appelle le constructeur de `Pokemon` : les sous-classes n'ont pas à réécrire l'initialisation des attributs, elles fixent seulement le type.
- Chaque sous-classe **redéfinit** `attaquer`. Quand on écrit `salameche.attaquer(...)`, Python cherche d'abord la méthode dans `PokemonFeu`, et ne remonte à `Pokemon` que s'il ne la trouve pas.
- Les trois méthodes `attaquer` se ressemblent beaucoup : c'est justement la question de l'exercice 10.
`salameche` est une instance des deux classes, car `PokemonFeu` **hérite** de `Pokemon` : un Pokémon Feu **est un** Pokémon. Il possède tous ses attributs et toutes ses méthodes (`est_ko`, `soigner`...). C'est pour cela qu'on peut le placer dans l'équipe d'un `Dresseur` sans rien changer à cette classe.
```python
def compter_par_type(equipe):
"""
Compte les Pokémon de chaque type.
:param equipe: (list) Liste de Pokémon
:return: (dict) Dictionnaire {type: nombre}
"""
compteur = {}
for pokemon in equipe:
if pokemon.type_pokemon in compteur:
compteur[pokemon.type_pokemon] += 1
else:
compteur[pokemon.type_pokemon] = 1
return compteur
```
---
### Exercice 10 : Refactoring
Il n'y a pas de réponse unique. Éléments de discussion :
- **Avec les sous-classes**, chaque type porte son propre comportement : c'est lisible, et l'on peut donner à un type des particularités qui ne se résument pas à des faiblesses. Mais le code d'`attaquer` est recopié trois fois, et ajouter un type oblige à écrire une nouvelle classe.
- **Avec le dictionnaire `FAIBLESSES`**, toutes les règles sont réunies au même endroit et ajouter une faiblesse se fait en une ligne, sans toucher au code. Il gère aussi naturellement un type faible contre plusieurs autres.
- Pour des **données** (qui bat qui), le dictionnaire est plus adapté. Les sous-classes sont intéressantes quand les types ont des **comportements** vraiment différents. On peut combiner les deux : des sous-classes qui consultent le dictionnaire.
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"><img alt="Licence Creative Commons" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" /></a> <br />Ce cours est mis à disposition selon les termes de la <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 4.0 International</a>.