PYTHON → ROBOTIQUE · 16

Séance 16 · Partie B · Algorithmique et maths

Programmation
orientée objet.

Un robot a un état (position, batterie) et des capacités (avancer, scanner). Jusqu’ici, l’état était un dictionnaire et les capacités des fonctions séparées. Un objet réunit les deux.

Durée : 2 séances · Objectifs : classes, attributs, méthodes, __init__, self, __repr__, opérateurs, héritage, quand (ne pas) utiliser les objets.

Ce que vous saurez faire à la fin
  • Définir une classe et créer des objets.
  • Modéliser un capteur, un vecteur, un robot, un simulateur.
  • Utiliser l’héritage pour partager du code entre classes proches.
  • Comprendre enfin ce que sont liste.append() et "abc".upper().

01 / Classes

Une classe est un moule, un objet est un exemplaire

  • class Robot: définit le moule.
  • Robot("Alpha") fabrique un objet : Python appelle __init__ pour l’initialiser.
  • self désigne l’objet en cours. r1.avancer(10) signifie Robot.avancer(r1, 10).
  • Chaque objet a ses propres attributs : r1 et r2 ne se mélangent pas.
self, ce mot partout

Quand vous écrivez r1.avancer(10), Python passe automatiquement r1 comme premier argument : c’est self. Dans la méthode, self.x est « le x de cet objet ». Sans self., x serait une variable locale qui disparaît à la fin de la méthode. Oublier self dans la liste des paramètres est l’erreur n°1 de cette séance (TypeError: avancer() takes 1 positional argument but 2 were given).

01 / Classes

Vous utilisiez déjà des objets

Tout est objet

En Python, chaque valeur est un objet : entier, chaîne, liste, fonction, module, classe. Le point . donne accès à ses attributs et méthodes. Écrire une classe, c’est ajouter un nouveau type à Python, avec ses propres méthodes. Rien de magique : list est une classe comme Robot, simplement écrite en C.

02 / Méthodes spéciales

__repr__, __str__ : un objet qui se présente

Sans __repr__

print(temp) afficherait <__main__.Capteur object at 0x7f…> : inutile. Définir __repr__ rend le débogage agréable ; convention : une chaîne qui ressemble au code pour recréer l’objet. Les méthodes entourées de doubles tirets bas (« dunder ») sont appelées par Python lui-même dans des situations précises : __init__ à la création, __repr__ à l’affichage, __len__ par len(), __eq__ par ==

02 / Méthodes spéciales

Surcharger les opérateurs : une classe Vecteur

Comparez avec la séance 14

Là, on avait des tuples et des fonctions ajouter(u, v). Ici, u + v. Le code qui utilise des vecteurs devient lisible comme des maths. C’est exactement ainsi que NumPy (séance 19) rend a + b possible sur des tableaux. Notez que les méthodes renvoient de nouveaux vecteurs sans modifier self : un vecteur immuable évite les surprises de la séance 06.

03 / Conception

Un objet qui en contient d’autres : le robot et ses capteurs

Composition

Un robot a des capteurs et des moteurs : on les crée dans __init__ et on y accède par self.distance, self.gauche. Chaque classe est petite, testable seule, et remplaçable : pour passer au vrai robot, on réécrit Capteur.lire et Moteur.regler pour parler au matériel, sans toucher à Robot.cycle. Cette séparation est le principe de conception le plus important en robotique : la logique ne dépend pas du matériel.

03 / Conception

Héritage : une classe qui en étend une autre

Quand hériter

Quand une classe est un cas particulier d’une autre (un capteur de distance est un capteur). La classe fille récupère tout de la mère et peut redéfinir ce qui diffère (lire). super() désigne la classe mère. L’intérêt : du code qui traite des Capteur en général (une boucle sur capteurs) fonctionne avec tous les types, y compris ceux inventés plus tard. C’est le polymorphisme. Règle de prudence : préférez la composition (« a un ») à l’héritage (« est un ») en cas de doute ; les hiérarchies profondes sont un piège classique.

03 / Conception

Le simulateur de la séance 15, en objets

Ce qu’on a gagné

Trois classes, chacune avec une responsabilité : la physique, la décision, l’orchestration. Pour tester un autre contrôleur, on écrit une classe avec une méthode commande(mobile) et rien d’autre ne change. Pour un mobile en 2D, on change Mobile. Pour enregistrer les résultats dans un fichier, Simulation. Comparez avec les lambda et variables globales de la séance 15 : c’était plus court, mais impossible à faire grandir.

03 / Conception

Détails utiles : attributs de classe, propriétés, dataclasses

Trois raccourcis à connaître

Un attribut de classe sert aux constantes et compteurs partagés. Le préfixe _ signale « ne pas toucher de l’extérieur » (Python ne l’interdit pas, la convention suffit). @property transforme une méthode en attribut calculé. @dataclass écrit pour vous le __init__ et le __repr__ d’une classe qui ne fait que porter des données : utilisez-le systématiquement pour ce cas. Le @ est un décorateur : une fonction qui modifie une fonction ou une classe. On en verra d’autres en séance 17.

04 / Défis

Défi ★ — Compte et pile

Consigne

1. Compte avec solde, deposer, retirer (refuse si insuffisant, renvoie un booléen), historique des opérations, __repr__.
2. Pile avec empiler, depiler (erreur si vide), sommet, __len__ (pour que len(pile) marche) et __bool__ ou __len__ pour que if pile: marche. Tester.

Correction
class Pile:
    def __init__(self):
        self._elements = []
    def empiler(self, x):
        self._elements.append(x)
    def depiler(self):
        if not self._elements:
            raise IndexError("pile vide")
        return self._elements.pop()
    def sommet(self):
        return self._elements[-1] if self._elements else None
    def __len__(self):
        return len(self._elements)          # rend aussi bool(pile) correct
    def __repr__(self):
        return f"Pile({self._elements})"

Une pile ne permet d’accéder qu’au dernier élément ajouté (LIFO). C’est la structure de la pile d’appels (séance 12) et du bouton « annuler ». Séance 18 pour son usage dans les algorithmes.

04 / Défis

Défi ★★ — Une flotte polymorphe

Consigne

Classe de base Vehicule (nom, position x, vitesse max, méthode avancer(dt) qui déplace de vitesse × dt, __repr__). Trois classes filles : Rover (vitesse réduite de moitié sur « sable », attribut terrain), Drone (a une altitude, decoller/atterrir, n’avance que s’il vole), Bateau (dérive : sa position subit un courant constant en plus). Créer une flotte, la faire avancer 10 pas dans une seule boucle, afficher. Puis plus_rapide(flotte) qui renvoie le véhicule le plus loin.

Correction
class Vehicule:
    def __init__(self, nom, vitesse_max):
        self.nom, self.vitesse_max, self.x = nom, vitesse_max, 0.0
    def vitesse(self):
        return self.vitesse_max
    def avancer(self, dt):
        self.x += self.vitesse() * dt
    def __repr__(self):
        return f"{type(self).__name__}({self.nom}, x={self.x:.1f})"

class Rover(Vehicule):
    def __init__(self, nom, vitesse_max, terrain="dur"):
        super().__init__(nom, vitesse_max); self.terrain = terrain
    def vitesse(self):
        return self.vitesse_max / 2 if self.terrain == "sable" else self.vitesse_max

class Drone(Vehicule):
    def __init__(self, nom, vitesse_max):
        super().__init__(nom, vitesse_max); self.altitude = 0
    def decoller(self): self.altitude = 10
    def atterrir(self): self.altitude = 0
    def vitesse(self):
        return self.vitesse_max if self.altitude > 0 else 0

class Bateau(Vehicule):
    def __init__(self, nom, vitesse_max, courant=0.3):
        super().__init__(nom, vitesse_max); self.courant = courant
    def avancer(self, dt):
        super().avancer(dt)
        self.x += self.courant * dt

flotte = [Rover("R1", 2), Rover("R2", 2, "sable"), Drone("D1", 5), Bateau("B1", 1.5)]
flotte[2].decoller()
for _ in range(10):
    for v in flotte:
        v.avancer(1)
print(flotte)
print("Le plus loin :", max(flotte, key=lambda v: v.x))

La boucle for v in flotte: v.avancer(1) ne sait pas quel type de véhicule elle manipule : chaque objet applique sa propre version. C’est le polymorphisme, et c’est ce qui permet d’ajouter un sous-marin demain sans toucher à la boucle. Remarquez la méthode vitesse() introduite dans la classe mère pour que les filles n’aient qu’à la redéfinir.

04 / Défis

Défi ★★★ — Esprit prépa : des fractions exactes

Consigne

Les flottants sont approchés (séance 13). Écrire une classe Fraction (numérateur, dénominateur entiers) toujours réduite (PGCD) avec dénominateur positif, supportant + − × /, la comparaison == <, __repr__, la conversion float(), et les opérations mixtes avec des entiers (Fraction(1, 2) + 1 et 1 + Fraction(1, 2)).

Vérifier : 1/3 + 1/6 == 1/2 exactement ; la somme 1/1 + 1/2 + … + 1/20 ; et calculer 0.1 + 0.2 == 0.3 en fractions. Comparer avec le module fractions de Python.

Correction et ouverture
class Fraction:
    def __init__(self, num, den=1):
        if den == 0:
            raise ZeroDivisionError("dénominateur nul")
        if den < 0:
            num, den = -num, -den
        g = gcd(num, den)
        self.num, self.den = num // g, den // g

    @staticmethod
    def _conv(x):
        return x if isinstance(x, Fraction) else Fraction(x)

    def __repr__(self):
        return f"{self.num}" if self.den == 1 else f"{self.num}/{self.den}"
    def __add__(self, o):
        o = Fraction._conv(o); return Fraction(self.num * o.den + o.num * self.den, self.den * o.den)
    def __sub__(self, o):
        o = Fraction._conv(o); return Fraction(self.num * o.den - o.num * self.den, self.den * o.den)
    def __mul__(self, o):
        o = Fraction._conv(o); return Fraction(self.num * o.num, self.den * o.den)
    def __truediv__(self, o):
        o = Fraction._conv(o); return Fraction(self.num * o.den, self.den * o.num)
    __radd__ = __add__
    __rmul__ = __mul__
    def __rsub__(self, o):  return Fraction._conv(o) - self
    def __rtruediv__(self, o): return Fraction._conv(o) / self
    def __eq__(self, o):
        o = Fraction._conv(o); return (self.num, self.den) == (o.num, o.den)
    def __lt__(self, o):
        o = Fraction._conv(o); return self.num * o.den < o.num * self.den
    def __float__(self):
        return self.num / self.den

print(Fraction(1, 3) + Fraction(1, 6) == Fraction(1, 2))
h = sum((Fraction(1, k) for k in range(1, 21)), Fraction(0))
print(h, float(h))
print(Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10))
print(1 + Fraction(1, 2), Fraction(1, 2) < 1)

Grâce à la réduction dans __init__, l’égalité se teste sur les couples (num, den). __radd__ gère 1 + f : Python essaie int.__add__(1, f), échoue, puis f.__radd__(1). La somme harmonique H₂₀ = 55835135/15519504 est exacte, ce qu’aucun flottant ne peut donner. Le module standard fractions.Fraction fait tout cela (et plus) ; l’avoir écrit soi-même est le meilleur moyen de comprendre les opérateurs. Ouverture : écrire de même une classe Polynome ou Matrice2x2 — vous verrez que le produit de matrices est exactement la composition des rotations de la séance 14.

05 / Vérification

Dans def avancer(self, d), que représente self ?

Deux questions supplémentaires

1. Quelle différence entre self.x et x dans une méthode ? self.x est un attribut de l’objet, persistant ; x une variable locale, perdue à la fin de la méthode.

2. Quand une classe fille définit une méthode de même nom que la mère, laquelle est appelée ? Celle de la fille ; elle peut appeler celle de la mère avec super().methode().

Référence

Les mots à retenir

MotDéfinition
ClasseModèle définissant attributs et méthodes.
Objet / instanceExemplaire créé à partir d’une classe.
AttributVariable attachée à un objet (self.x).
MéthodeFonction attachée à une classe ; reçoit self.
__init__Méthode d’initialisation, appelée à la création.
HéritageUne classe fille étend une classe mère.
PolymorphismeMême appel, comportement selon le type réel.
CompositionUn objet contient d’autres objets.

Pour continuer

Fin de la partie B

Vous savez raisonner sur un algorithme, prouver, mesurer, simuler, modéliser. La partie C affûte vos outils Python avant de passer au matériel.

À faire chez soi

Documentation

Tutoriel officiel : classes · Méthodes spéciales · dataclasses

← Séance 15SommaireSéance 17 : Itérateurs, générateurs, compréhensions →