Module L02 · Partie F · Coder comme un professionnel
Programmation orientée objet, pour de vrai.
Un objet, c’est des données et les opérations qui ont le droit de les toucher, réunies derrière une porte. La POO n’est pas une syntaxe : c’est une façon de découper un problème en responsabilités. Ce module va des quatre piliers aux patrons de conception que vous retrouverez dans ROS, PyTorch et tous les gros logiciels.
Durée : 3 séances · Prérequis : séance 16 du collège, module L01. Objectifs : encapsulation, héritage, polymorphisme, abstraction ; méthodes spéciales ; composition vs héritage ; dataclasses ; patrons Stratégie, Observateur, État, Fabrique ; principes SOLID.
Ce que vous saurez faire à la fin
- Modéliser un système (robot, jeu, simulation) en classes aux responsabilités claires.
- Choisir entre héritage et composition, et justifier.
- Écrire une hiérarchie abstraite avec
abcet un polymorphisme sansif type(x) == …. - Reconnaître et appliquer cinq patrons de conception.
Références : Design Patterns (Gamma et al.), Fluent Python (Ramalho), cours MIT 6.0001, freeCodeCamp « Object Oriented Programming ».
Fiche de cours · Définitions
Le vocabulaire de la programmation orientée objet
self désigne l’instance sur laquelle une méthode est appelée._prive). L’invariant de classe est une propriété de l’état vraie après le constructeur et après chaque méthode publique (ex. « le solde est ≥ 0 », « la pile de la file est triée »).obj.methode() exécute le code de la classe réelle de obj (liaison dynamique).@abstractmethod) et ne peut pas être instanciée ; elle définit un contrat. En Python, le contrat peut aussi être implicite : tout objet qui a les bonnes méthodes convient (« s’il marche comme un canard… »). Un Protocol (typing) rend ce contrat vérifiable statiquement.self.moteur = Moteur()). Héritage : un objet est un autre. Règle : préférer la composition ; hériter seulement si la relation « est un » est vraie et que le code appelant doit pouvoir substituer.Fiche de cours · Formules
Les règles et « formules » de conception
| Principe | Énoncé | Test rapide |
|---|---|---|
| S — Responsabilité unique | Une classe a une seule raison de changer | Pouvez-vous décrire la classe sans « et » ? |
| O — Ouvert/fermé | Ouvert à l’extension, fermé à la modification | Ajouter un cas oblige-t-il à modifier un if isinstance ? |
| L — Substitution de Liskov | Toute instance de B doit pouvoir remplacer une instance de A sans casser le programme | B affaiblit-elle une postcondition ou renforce-t-elle une précondition de A ? Alors ce n’est pas un héritage valide |
| I — Ségrégation des interfaces | Plusieurs petites interfaces valent mieux qu’une grosse | Une classe implémente-t-elle des méthodes « vides » ? |
| D — Inversion des dépendances | Dépendre d’abstractions, pas d’implémentations | Peut-on tester la classe avec un faux capteur ? |
obj.m dans classe(obj), puis ses parents, dans l’ordre Classe.__mro__ (linéarisation C3) super().m() appelle le suivant dans cet ordre, pas forcément la classe parente écriteMéthodes spéciales à connaître : __init__ (construction), __repr__ (débogage, non ambigu) / __str__ (affichage), __eq__ + __hash__ (égalité et utilisation comme clé), __lt__ (tri), __len__, __iter__, __getitem__, __add__/__mul__ (opérateurs), __enter__/__exit__ (with), __call__.
Fiche de cours · Théorèmes et démonstrations
Ce qu’on peut prouver sur des objets
_prive et des propriétés en lecture seule.r.largeur = 2; r.hauteur = 3; assert r.aire() == 6 est correct pour tout Rectangle (postcondition des setters : l’autre dimension est inchangée). Pour un Carré, le setter de hauteur doit aussi changer la largeur (sinon l’objet n’est plus un carré), donc aire() vaut 9 : l’assertion échoue. Carré renforce la postcondition des setters de manière incompatible ; la substitution est impossible. Conclusion : « est un » en géométrie n’implique pas « est un » en programmation quand il y a mutation. Solutions : objets immuables, ou Carré et Rectangle frères sous une abstraction Forme.a == b alors on doit avoir hash(a) == hash(b) ; sinon les dict et set sont incohérents.__eq__ impose de redéfinir __hash__ sur les mêmes attributs (Python met d’ailleurs __hash__ = None automatiquement quand on redéfinit seulement __eq__). Un objet mutable utilisé comme clé viole cette règle dès qu’il change : ne hacher que des objets immuables (@dataclass(frozen=True)).Fiche de cours · Méthodes
Méthodes de conception et pièges
deposer, pas set_solde). (4) Décider ce qui est immuable. (5) Écrire __repr__ et __eq__ dès le début (tests lisibles). (6) Écrire les tests de l’invariant avant l’implémentation.if isinstance par du polymorphisme. Chaque branche du if devient une méthode de la sous-classe correspondante ; l’appelant écrit obj.action() sans connaître le type. Ajouter un type n’exige plus de modifier l’appelant (principe ouvert/fermé).Pièges : attribut de classe mutable partagé par toutes les instances (class A: items = []) ; oublier super().__init__() ; redéfinir __eq__ sans __hash__ ; hériter pour « réutiliser du code » ; classes « Manager » fourre-tout ; getters/setters triviaux au lieu d’attributs publics ou de @property.
Fiche de cours · Exercices corrigés
Exercices corrigés
Pile avec empiler, depiler, sommet, est_vide. Énoncer l’invariant, les préconditions, et prouver que depiler renvoie le dernier élément empilé non encore dépilé._t. Invariant : _t contient les éléments non dépilés, dans l’ordre d’empilement (le dernier empilé en dernière position). Précondition de depiler/sommet : pile non vide (sinon lever IndexError). Preuve : empiler(x) fait _t.append(x) — x devient le dernier, invariant conservé. depiler fait _t.pop() qui retire et renvoie le dernier élément de _t, qui est par l’invariant le dernier empilé non dépilé ; les autres restent dans l’ordre : invariant conservé.Capteur (abstrait, lire()), Ultrason, Lidar ; un Robot possède une liste de capteurs et une méthode distance_min(). Écrire le code sans aucun isinstance, puis expliquer comment ajouter une Camera sans toucher à Robot.from abc import ABC, abstractmethod
class Capteur(ABC):
@abstractmethod
def lire(self) -> float: ... # distance en m
class Ultrason(Capteur):
def lire(self): return 0.42 # (mesure réelle sur PC)
class Lidar(Capteur):
def lire(self): return 1.7
class Robot:
def __init__(self, capteurs): self.capteurs = list(capteurs)
def distance_min(self): return min(c.lire() for c in self.capteurs)Ajouter class Camera(Capteur) avec sa méthode lire suffit : Robot n’appelle que le contrat lire(). C’est le principe ouvert/fermé rendu concret par le polymorphisme.@dataclass(frozen=True) class Point: x: float; y: float peut servir de clé de dictionnaire, et pourquoi la version non gelée ne le peut pas.frozen=True, dataclass génère __eq__ (comparaison champ à champ) et __hash__ (hachage du tuple des champs), et interdit la mutation : la cohérence égalité/hachage (Proposition 3) est garantie pour toute la vie de l’objet. Sans frozen, dataclass génère __eq__ mais met __hash__ = None : l’objet est non hachable (TypeError), précisément parce qu’une mutation après insertion rendrait le dict incohérent.01 / Les quatre piliers
Encapsulation : l’objet protège son état
Pourquoi cacher ?
Si tout le programme peut écrire batterie._charge = -50, il faut vérifier la validité partout. Si seule la classe peut l’écrire, on vérifie une fois, dans le setter. L’invariant « 0 ≤ charge ≤ max » est garanti par construction. Python ne force rien (pas de private comme en Java/C++), mais la convention _nom est respectée par tous, et @property permet de commencer avec un attribut simple et d’ajouter une validation plus tard sans changer l’interface. Un double underscore __nom déclenche le « name mangling » (_Batterie__nom) : à réserver pour éviter les collisions dans les hiérarchies.
01 / Les quatre piliers
Héritage et polymorphisme : un même message, des réponses différentes
Ce que le polymorphisme remplace
Sans lui, on écrirait if isinstance(f, Rectangle): … elif isinstance(f, Disque): … partout où on manipule des formes, et il faudrait modifier tous ces if à chaque nouvelle forme. Avec lui, ajouter Triangle = écrire une classe, point. C’est le principe ouvert/fermé : ouvert à l’extension, fermé à la modification. Le MRO (method resolution order) est l’ordre dans lequel Python cherche une méthode : Carre → Rectangle → Forme → object.
01 / Les quatre piliers
Abstraction : forcer le contrat avec abc
Une classe abstraite ne s’instancie pas ; elle dit « voici ce que mes filles doivent fournir ». Le code qui utilise un Capteur n’a besoin que du contrat, jamais des détails. En C++ : méthodes virtuelles pures ; en Java : interfaces ; en OCaml : signatures de modules.
02 / Le modèle objet de Python
Méthodes spéciales : parler le langage de Python
Tableau des méthodes spéciales
| Syntaxe | Méthode |
|---|---|
str(x), print(x) | __str__ (sinon __repr__) |
x + y, x - y, x * y, x / y, x // y, x % y, x ** y | __add__, __sub__, __mul__, __truediv__, __floordiv__, __mod__, __pow__ |
x < y, x <= y, x == y | __lt__, __le__, __eq__ |
len(x), x[i], x[i] = v, i in x | __len__, __getitem__, __setitem__, __contains__ |
for a in x | __iter__ (ou __getitem__) |
x() | __call__ |
with x: | __enter__, __exit__ |
x.attr inconnu | __getattr__ |
C’est ainsi que NumPy fait a @ b, que pathlib fait Path("a") / "b", et que PyTorch rend tenseur.backward() possible. Une classe bien écrite se comporte comme un type natif.
02 / Le modèle objet de Python
Dataclasses, gestionnaires de contexte, itérateurs
Quand utiliser quoi
- dataclass : un objet qui est surtout des données (mesure, point, configuration). Génère
__init__,__repr__,__eq__, etorder=Trueles comparaisons ;frozen=Truele rend hashable et immuable. - Gestionnaire de contexte (
with) : tout ce qui s’ouvre et doit se fermer — fichier, port série, verrou, chronomètre, connexion. Garantit la fermeture même en cas d’exception. - Itérateur : un objet qu’on peut parcourir.
__iter__renvoyant un générateur est la façon la plus simple. - NamedTuple (
typing.NamedTuple) : un tuple avec des noms, encore plus léger qu’une dataclass gelée.
03 / Conception
Composition plutôt qu’héritage
La règle
Héritez quand la relation est « est un » et que la sous-classe peut remplacer la classe mère partout (principe de substitution de Liskov). Composez quand la relation est « a un » ou « utilise un ». L’héritage crée un couplage fort : changer la mère change toutes les filles. La composition permet d’assembler et de remplacer des pièces — comme un vrai robot. Le célèbre contre-exemple : class Carre(Rectangle) avec des setters de largeur et hauteur indépendants viole Liskov (fixer la largeur d’un carré change sa hauteur : une fonction écrite pour Rectangle se trompera).
03 / Conception
Patron Stratégie : l’algorithme est un objet qu’on branche
Les fonctions Python étant des objets, une stratégie peut aussi être une simple fonction passée en paramètre (key= de sorted est une stratégie !). Une classe devient utile quand la stratégie a un état ou plusieurs méthodes.
03 / Conception
Patron Observateur : réagir sans coupler
Où vous le retrouverez
Les interfaces graphiques (bouton.on_click(...)), le JavaScript (addEventListener), ROS (un topic auquel des nœuds s’abonnent), Qt (signaux/slots), les interruptions matérielles (module L18). L’émetteur ne sait pas qui écoute : c’est ce découplage qui permet d’ajouter un enregistreur, un affichage, un freinage d’urgence sans modifier le capteur.
03 / Conception
Patron État : chaque état est une classe
Comparez avec la version « une grosse fonction et des if etat == … » de la séance 28 : ici chaque état est isolé, testable seul, et ajouter un état ne touche pas aux autres. Pour 3 états, c’est plus long ; pour 15 états et 40 transitions (un vrai robot), c’est la seule façon de rester sain.
03 / Conception
Fabrique et registre : créer sans connaître la classe
Principes SOLID, en une ligne chacun
- Single responsibility : une classe, une raison de changer.
- Open/closed : étendre sans modifier (polymorphisme, registre).
- Liskov : une sous-classe doit pouvoir remplacer sa mère partout.
- Interface segregation : plusieurs petites interfaces valent mieux qu’une grosse.
- Dependency inversion : dépendre d’abstractions (Capteur), pas d’implémentations (Ultrason) ; injecter les dépendances.
Ces principes ne sont pas des lois : ce sont des heuristiques qui rendent le code facile à changer. Le test ultime : « si le client demande X demain, combien de fichiers dois-je toucher ? »
Cours
Cours 1 — Objets, classes, instances : le vocabulaire exact
| Terme | Définition | En Python |
|---|---|---|
| Classe | Le moule : décrit les attributs et les méthodes | class Robot: |
| Instance / objet | Un exemplaire concret créé à partir de la classe | r = Robot() |
| Attribut d’instance | Donnée propre à chaque objet | self.vitesse = 0 dans __init__ |
| Attribut de classe | Donnée partagée par toutes les instances | NB_ROUES = 2 au niveau de la classe |
| Méthode | Fonction attachée à la classe ; reçoit l’instance en premier argument | def avancer(self, d): |
| Constructeur | Méthode appelée à la création | __init__ (initialise ; __new__ crée, rarement utile) |
| Méthode de classe / statique | Reçoit la classe / ne reçoit rien | @classmethod def depuis_json(cls, s) ; @staticmethod |
| Propriété | Méthode déguisée en attribut | @property |
Ce qui se passe vraiment. r.avancer(3) est du sucre pour Robot.avancer(r, 3) : Python cherche avancer dans type(r) puis ses parents (MRO), et passe r comme self. Un objet est un dictionnaire d’attributs (r.__dict__) plus un pointeur vers sa classe. C’est pourquoi on peut ajouter un attribut à la volée — et pourquoi __slots__ existe pour l’interdire et économiser la mémoire.
Cours
Cours 2 — Concevoir : de l’énoncé aux classes (méthode CRC)
Méthode. Pour passer d’un problème à une architecture objet : 1) souligner les noms de l’énoncé (candidats classes), les verbes (candidats méthodes) ; 2) pour chaque classe, écrire une fiche CRC : Classe, Responsabilités (ce qu’elle sait et ce qu’elle fait — 3 à 5 lignes maximum), Collaborateurs (les classes dont elle a besoin) ; 3) éliminer les classes sans responsabilité, fusionner celles qui en partagent, séparer celles qui en ont trop ; 4) décider pour chaque lien : héritage (« est un ») ou composition (« a un ») ; 5) écrire les signatures avant les corps.
| Classe | Responsabilités | Collaborateurs |
|---|---|---|
Robot | Connaît sa pose ; coordonne capteurs et moteurs ; exécute un cycle de contrôle | Capteur, Moteur, Controleur |
Capteur (abstrait) | Fournit une mesure ; connaît son nom et son unité | — |
Ultrason(Capteur) | Mesure une distance par écho | Broche |
Controleur (abstrait) | Transforme des mesures en commandes | Capteur (lecture), Moteur (écriture) |
Journal | Enregistre les événements horodatés ; exporte en CSV | — |
Signaux d’une mauvaise conception : une classe « Gestionnaire » qui fait tout ; des isinstance en cascade (remplacer par du polymorphisme) ; une sous-classe qui redéfinit une méthode pour ne rien faire (l’héritage est faux) ; deux classes qui accèdent aux attributs privés l’une de l’autre (les fusionner ou clarifier l’interface) ; un constructeur à 12 paramètres (introduire un objet de configuration).
Cours
Cours 3 — Égalité, hachage, copie : les pièges des objets mutables
Retenez : définir __eq__ supprime le __hash__ par défaut ; si vous voulez des objets égaux et utilisables comme clés, définissez les deux sur des attributs qui ne changent pas, ou utilisez @dataclass(frozen=True) qui fait tout correctement.
TP guidé
TP — Simulateur de robot en objets, testé (sur PC, 1 h 30)
- Structure. Dans un dossier
tp-pooavec venv et pytest (TP du module L01) :tp-poo/ ├── simu/__init__.py ├── simu/capteurs.py # Capteur (ABC), CapteurSimule, CapteurBruite ├── simu/robot.py # Moteur, Robot (composition) ├── simu/controle.py # Strategie (ABC), Arret, EvitementSimple, machine à états ├── simu/evenements.py # Evenement (observateur) └── tests/test_*.py - Capteurs.
Capteurabstrait aveclire() -> floatetnom.CapteurSimule(valeurs: list)renvoie les valeurs une à une puis répète la dernière.CapteurBruite(capteur, sigma)décore un autre capteur en ajoutant un bruit gaussien (composition : il contient un capteur). Test : la moyenne de 1000 lectures bruitées est proche de la valeur. - Robot.
Moteur.regler(v)sature dans [−1, 1] et émet un événementvitesse_changee.Robotpossède deux moteurs, un capteur avant, une pose (x, y, θ) intégrée parcycle(dt)avec la cinématique différentielle (séance 27). Test : à vitesses égales pendant 1 s, x avance de v ; à vitesses opposées, θ tourne et x ne bouge pas. - Stratégies et états.
Strategie.decider(distance) -> (v_g, v_d).Arretrenvoie (0, 0).EvitementSimple: machine à états EXPLORER / RECULER / TOURNER (patron État : une classe par état). Test : depuis EXPLORER, une distance < 20 fait passer en RECULER ; après 5 cycles, TOURNER ; puis EXPLORER quand la distance > 60. - Observateur. Un
Journals’abonne àvitesse_changeeet au changement d’état ;exporter_csv()écritt;evenement;valeur. Test : après un scénario de 50 cycles, le CSV a le bon nombre de lignes et les états apparaissent dans l’ordre attendu. - Vérifier.
Attendu : ≥ 8 tests verts, zéro avertissement. Puis remplacezpytest -v ruff check . && mypy simuEvitementSimplepar une stratégie de votre invention sans toucher à Robot : si vous devez modifierrobot.py, la conception est à revoir (principe ouvert/fermé). - Livrable. Le dépôt, plus un schéma des classes (boîtes et flèches : héritage en flèche pleine, composition en losange) sur une page, et vos fiches CRC.
Exercices
Exercices auto-corrigés — classes et méthodes spéciales
Exercice 1 — Une classe Duree
Complétez Duree (secondes entières) : construction, __add__, __lt__, __eq__, __str__ au format h:mm:ss, et une méthode de classe depuis_texte("1:02:03").
Correction
class Duree:
def __init__(self, secondes):
if secondes < 0: raise ValueError("durée négative")
self.secondes = int(secondes)
def __add__(self, o): return Duree(self.secondes + o.secondes)
def __lt__(self, o): return self.secondes < o.secondes
def __eq__(self, o): return isinstance(o, Duree) and self.secondes == o.secondes
def __str__(self):
h, r = divmod(self.secondes, 3600); m, s = divmod(r, 60); return f"{h}:{m:02d}:{s:02d}"
@classmethod
def depuis_texte(cls, t):
h, m, s = map(int, t.split(":")); return cls(3600 * h + 60 * m + s)Exercice 2 — Polymorphisme sans isinstance
Trois formes de messages arrivent d’un robot : Position(x, y), Batterie(pourcent), Alerte(texte). Écrivez une classe de base Message avec une méthode abstraite resume(), les trois sous-classes, et la fonction rapport(messages) qui concatène les résumés — sans aucun isinstance.
Correction
class Message(ABC):
@abstractmethod
def resume(self) -> str: ...
class Position(Message):
def __init__(self, x, y): self.x, self.y = x, y
def resume(self): return f"pos({self.x}, {self.y})"
class Batterie(Message):
def __init__(self, p): self.p = p
def resume(self): return f"batt {self.p}%"
class Alerte(Message):
def __init__(self, t): self.t = t
def resume(self): return f"ALERTE {self.t}"
def rapport(messages): return " | ".join(m.resume() for m in messages)Exercices
Exercices auto-corrigés — patrons de conception
Exercice 3 — Observateur avec désabonnement
Complétez Evenement : abonner(cb) renvoie une fonction qui désabonne ; emettre(*args) appelle les abonnés dans l’ordre d’abonnement ; un abonné qui lève une exception ne doit pas empêcher les suivants (l’exception est comptée dans erreurs).
Correction
def abonner(self, cb):
self._abonnes.append(cb)
def desabonner():
if cb in self._abonnes: self._abonnes.remove(cb)
return desabonner
def emettre(self, *args):
for cb in list(self._abonnes):
try: cb(*args)
except Exception: self.erreurs += 1Exercice 4 — Fabrique par registre
Un décorateur @forme("cercle") enregistre des classes ; creer(description) construit l’objet à partir d’un dictionnaire {"type": ..., **params} et lève KeyError avec le nom inconnu sinon. Toutes les formes ont aire().
Correction
def forme(nom):
def enregistrer(cls): REGISTRE[nom] = cls; return cls
return enregistrer
def creer(description):
d = dict(description); t = d.pop("type")
if t not in REGISTRE: raise KeyError(t)
return REGISTRE[t](**d)04 / Défis
Défi ★ — Une hiérarchie de comptes
Consigne
Classe abstraite Compte (solde encapsulé, deposer, retirer abstrait, __repr__). CompteCourant : découvert autorisé jusqu’à −500. Livret : jamais négatif, méthode interets(taux). Une fonction total(comptes) polymorphe. Tests pour chaque règle.
Correction
class Compte(ABC):
def __init__(self, titulaire, solde=0.0):
self.titulaire, self._solde = titulaire, float(solde)
@property
def solde(self): return self._solde
def deposer(self, m):
if m <= 0: raise ValueError("montant positif attendu")
self._solde += m
@abstractmethod
def retirer(self, m) -> bool: ...
def __repr__(self): return f"{type(self).__name__}({self.titulaire}, {self._solde:.2f})"
class CompteCourant(Compte):
DECOUVERT = -500
def retirer(self, m):
if self._solde - m < self.DECOUVERT: return False
self._solde -= m; return True
class Livret(Compte):
def retirer(self, m):
if m > self._solde: return False
self._solde -= m; return True
def interets(self, taux): self._solde *= 1 + taux
def total(comptes): return sum(c.solde for c in comptes)
c, l = CompteCourant("Ana", 100), Livret("Ana", 1000)
assert c.retirer(550) and c.solde == -450 and not c.retirer(100)
assert not l.retirer(2000) and l.retirer(1000) and l.solde == 0
l.deposer(100); l.interets(0.03); assert abs(l.solde - 103) < 1e-9
assert total([c, l]) == -450 + 10304 / Défis
Défi ★★ — Un simulateur de jeu à composants
Consigne
Modélisez un jeu où des entités (joueur, monstre, tourelle) sont composées de composants : Position, Vie, Attaque, Deplacement. Une entité n’a pas forcément tous les composants (la tourelle ne se déplace pas). Une boucle de jeu de 10 tours : les entités avec Deplacement se rapprochent du joueur, celles avec Attaque frappent si à distance ≤ 1, la vie descend, les morts disparaissent. Pas d’héritage entre entités : uniquement de la composition et un dictionnaire de composants.
Correction (extrait)
from dataclasses import dataclass
@dataclass
class Position: x: int; y: int
@dataclass
class Vie: points: int
@dataclass
class Attaque: degats: int
@dataclass
class Deplacement: vitesse: int
def distance(a, b):
pa, pb = a.composants["position"], b.composants["position"]
return abs(pa.x - pb.x) + abs(pa.y - pb.y)
def tour(entites, joueur):
for e in entites:
if e is joueur: continue
if e.a("deplacement"):
p, pj = e.composants["position"], joueur.composants["position"]
v = e.composants["deplacement"].vitesse
p.x += max(-v, min(v, pj.x - p.x)); p.y += max(-v, min(v, pj.y - p.y))
if e.a("attaque") and distance(e, joueur) <= 1:
joueur.composants["vie"].points -= e.composants["attaque"].degats
entites[:] = [e for e in entites if not e.a("vie") or e.composants["vie"].points > 0]
joueur = Entite("joueur", position=Position(0, 0), vie=Vie(20))
monstre = Entite("monstre", position=Position(5, 0), vie=Vie(5), attaque=Attaque(2), deplacement=Deplacement(1))
tourelle = Entite("tourelle", position=Position(0, 1), attaque=Attaque(1))
entites = [joueur, monstre, tourelle]
for t in range(10):
tour(entites, joueur)
print(joueur.composants["vie"])C’est l’architecture Entité-Composant-Système (ECS) des moteurs de jeu (Unity, Bevy) : les données dans des composants, la logique dans des systèmes qui parcourent les entités possédant tel composant. Elle évite l’explosion des hiérarchies (« MonstreVolantQuiTire » hérite de quoi ?).
04 / Défis
Défi ★★★ — Esprit prépa : un moteur d’expressions symboliques
Consigne
Construisez une hiérarchie Expr : Const, Var, Add, Mul, Pow (exposant entier). Chaque nœud sait : s’évaluer avec un dictionnaire de valeurs, se dériver par rapport à une variable (renvoie une nouvelle Expr), se simplifier (0·x → 0, 1·x → x, x + 0 → x, constantes repliées) et s’afficher. Surchargez + * ** pour écrire x = Var("x"); f = 3 * x**2 + 2 * x + 1. Vérifiez que f.derivee("x") simplifiée affiche 6*x + 2 et que f.evaluer({"x": 2}) vaut 17.
Correction
class Const(Expr):
def __init__(self, v): self.v = v
def evaluer(self, env): return self.v
def derivee(self, x): return Const(0)
def simplifier(self): return self
def __repr__(self): return str(self.v)
class Var(Expr):
def __init__(self, nom): self.nom = nom
def evaluer(self, env): return env[self.nom]
def derivee(self, x): return Const(1 if x == self.nom else 0)
def simplifier(self): return self
def __repr__(self): return self.nom
class Add(Expr):
def __init__(self, a, b): self.a, self.b = a, b
def evaluer(self, env): return self.a.evaluer(env) + self.b.evaluer(env)
def derivee(self, x): return Add(self.a.derivee(x), self.b.derivee(x))
def simplifier(self):
a, b = self.a.simplifier(), self.b.simplifier()
if isinstance(a, Const) and isinstance(b, Const): return Const(a.v + b.v)
if isinstance(a, Const) and a.v == 0: return b
if isinstance(b, Const) and b.v == 0: return a
return Add(a, b)
def __repr__(self): return f"{self.a} + {self.b}"
class Mul(Expr):
def __init__(self, a, b): self.a, self.b = a, b
def evaluer(self, env): return self.a.evaluer(env) * self.b.evaluer(env)
def derivee(self, x): # (uv)' = u'v + uv'
return Add(Mul(self.a.derivee(x), self.b), Mul(self.a, self.b.derivee(x)))
def simplifier(self):
a, b = self.a.simplifier(), self.b.simplifier()
if isinstance(a, Const) and isinstance(b, Const): return Const(a.v * b.v)
for u, v in ((a, b), (b, a)):
if isinstance(u, Const):
if u.v == 0: return Const(0)
if u.v == 1: return v
if isinstance(a, Const) and isinstance(b, Mul) and isinstance(b.a, Const):
return Mul(Const(a.v * b.a.v), b.b).simplifier()
return Mul(a, b)
def __repr__(self):
def p(e): return f"({e})" if isinstance(e, Add) else str(e)
return f"{p(self.a)}*{p(self.b)}"
class Pow(Expr):
def __init__(self, a, n): self.a, self.n = a, n
def evaluer(self, env): return self.a.evaluer(env) ** self.n
def derivee(self, x): # (u^n)' = n u^(n-1) u'
return Mul(Mul(Const(self.n), Pow(self.a, self.n - 1)), self.a.derivee(x))
def simplifier(self):
a = self.a.simplifier()
if self.n == 0: return Const(1)
if self.n == 1: return a
if isinstance(a, Const): return Const(a.v ** self.n)
return Pow(a, self.n)
def __repr__(self): return f"{self.a}**{self.n}"
x = Var("x")
f = 3 * x**2 + 2 * x + 1
print(f, "=", f.evaluer({"x": 2}))
print("f' =", f.derivee("x").simplifier())Vous venez d’écrire le cœur d’un système de calcul formel (SymPy fait la même chose avec des milliers de règles) et le cœur de la différentiation automatique qui fait tourner PyTorch : un graphe d’expressions dérivé mécaniquement par la règle de la chaîne. Module L14 pour la suite.
05 / Vérification
Un Robot et un Moteur : quelle relation ?
Deux questions supplémentaires
1. Que garantit une méthode @abstractmethod ? Qu’aucune sous-classe ne peut être instanciée sans l’avoir définie : le contrat est vérifié à la création de l’objet, pas au premier appel raté.
2. Pourquoi field(default_factory=list) et pas = [] ? Parce qu’une valeur par défaut est créée une fois à la définition : toutes les instances partageraient la même liste.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Encapsulation | Cacher l’état, l’exposer par des méthodes qui maintiennent les invariants. |
| Héritage | Une classe étend une autre ; relation « est un ». |
| Polymorphisme | Un même appel exécute un code différent selon la classe de l’objet. |
| Abstraction | Classe qui définit un contrat sans l’implémenter (abc). |
| Composition | Un objet en contient d’autres ; relation « a un ». |
| Liskov | Une sous-classe doit être substituable à sa mère. |
| Stratégie | Patron : l’algorithme est un objet interchangeable. |
| Observateur | Patron : des abonnés réagissent aux événements d’un émetteur. |
| État | Patron : chaque état est une classe avec son comportement. |
| Fabrique | Patron : créer des objets à partir d’une description. |
| ECS | Entité-Composant-Système, architecture des moteurs de jeu. |
Pour continuer
Vous concevez des systèmes
Module suivant : implémenter soi-même les structures de données — listes chaînées, arbres, tas, tables de hachage — pour comprendre ce que coûte vraiment chaque opération.
À faire chez soi
- Reprendre le simulateur de robot (séance 15) avec les patrons État et Observateur.
- Lire le chapitre « Composition over inheritance » du Gang of Four ou l’article Wikipédia sur SOLID, puis relire votre code.
- Sur PC : explorer le code source de
pathlib(python -c "import pathlib; print(pathlib.__file__)") et repérer les méthodes spéciales.