🏛️ Le cours : classes et objets
Une classe est un modèle qui décrit un genre d'objets : les attributs (les données que chaque objet porte) et les méthodes (ce qu'il sait faire). Un objet, ou instance, est un exemplaire fabriqué à partir de la classe. En Python, tout est objet : un entier, un texte, une liste ont des méthodes ("nsi".upper(), liste.append(1)) ; les classes permettent de créer ses propres types.
| Mot | Signification | Exemple |
| classe | le modèle | Personne |
| instance (objet) | un exemplaire de la classe | eleve = Personne("Ada", 17) |
| attribut | une donnée stockée dans l'objet | self.nom |
| méthode | une fonction définie dans la classe, qui agit sur l'objet | def se_presenter(self): |
self | l'objet lui-même, premier paramètre de toute méthode | self.age += 1 |
| constructeur | la méthode __init__, appelée à la création | Personne("Ada", 17) |
Définir une classe et créer des objets
On définit une classe avec class Nom: (par convention en CamelCase). La méthode spéciale __init__ est appelée automatiquement quand on crée un objet : elle y range les valeurs de départ dans des attributs self.nom = .... Chaque méthode reçoit l'objet en premier paramètre, nommé self ; à l'appel, on ne le passe pas : eleve.se_presenter() envoie eleve comme self. Chaque objet a ses propres attributs.
class Personne:
def __init__(self, nom, age):
self.nom = nom
self.age = age
def se_presenter(self):
return f"Je m'appelle {self.nom} et j'ai {self.age} ans."
eleve = Personne("Ada", 17)
prof = Personne("Lin", 40)
print(eleve.se_presenter())
print(prof.nom, prof.age)
eleve.age += 1
print(eleve.age, prof.age)
AffichageJe m'appelle Ada et j'ai 17 ans.
Lin 40
18 40
Des méthodes qui agissent sur l'objet
Une méthode lit et modifie les attributs de l'objet grâce à self. Elle peut renvoyer une valeur (comme debiter, qui dit si l'opération a réussi) ou simplement modifier l'état (comme crediter).
class Compte:
def __init__(self, solde=0):
self.solde = solde
def crediter(self, montant):
self.solde += montant
def debiter(self, montant):
if montant > self.solde:
return False
self.solde -= montant
return True
c = Compte(100)
c.crediter(50)
print(c.debiter(500), c.solde)
print(c.debiter(120), c.solde)
AffichageFalse 150
True 30
Attributs de classe et attributs d'instance
Un attribut défini dans la classe, hors de toute méthode, est un attribut de classe : il est partagé par tous les objets (compteur d'instances, constante). Un attribut défini avec self. est propre à chaque objet. On modifie l'attribut de classe en passant par le nom de la classe.
class Compte:
nb_comptes = 0
def __init__(self, solde=0):
self.solde = solde
Compte.nb_comptes += 1
c1 = Compte(100)
c2 = Compte(50)
print(c1.solde, c2.solde)
print(Compte.nb_comptes, c1.nb_comptes)
Afficher un objet : __str__ et __repr__
Sans précaution, print(objet) affiche un texte peu parlant (du genre <... object at 0x7f...>). On définit __str__ pour le texte destiné à l'utilisateur (utilisé par print et str) et __repr__ pour une représentation de développeur, plus précise (utilisée dans les listes et la console).
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Point({self.x}, {self.y})"
p = Point(3, 4)
print(p)
print([p, Point(0, 1)])
print(repr(p), str(p))
Affichage(3, 4)
[Point(3, 4), Point(0, 1)]
Point(3, 4) (3, 4)
Encapsulation : protéger l'état d'un objet
Pour éviter qu'on modifie n'importe comment les attributs d'un objet, on adopte des conventions : un nom qui commence par _ signale un attribut interne (à ne pas toucher de l'extérieur) et __ déclenche en plus une transformation du nom (*name mangling*). Python n'impose rien de strict : c'est une convention de politesse. Pour contrôler la lecture et l'écriture d'un attribut, on utilise @property (lecture) et @nom.setter (écriture) : le code appelant écrit c.solde = 150 comme avec un attribut ordinaire, mais une vérification s'exécute en coulisses.
class CompteBancaire:
def __init__(self, solde=0):
self._solde = solde
@property
def solde(self):
return self._solde
@solde.setter
def solde(self, valeur):
if valeur < 0:
raise ValueError("solde négatif interdit")
self._solde = valeur
c = CompteBancaire(100)
c.solde = 150
print(c.solde)
try:
c.solde = -5
except ValueError as e:
print("Erreur :", e)
Affichage150
Erreur : solde négatif interdit
class Coffre:
def __init__(self):
self.__code = 1234
c = Coffre()
print(hasattr(c, "__code"), hasattr(c, "_Coffre__code"))
Le nom __code est transformé en _Coffre__code : on évite les collisions entre classes, mais on n'obtient pas une vraie confidentialité.
Méthodes de classe et méthodes statiques
@classmethod définit une méthode qui reçoit la classe (cls) au lieu de l'objet : elle sert souvent de constructeur alternatif. @staticmethod définit une fonction rangée dans la classe qui ne reçoit ni objet ni classe : un simple outil lié au sujet de la classe.
class Cercle:
PI = 3.14159
def __init__(self, rayon):
self.rayon = rayon
@classmethod
def depuis_diametre(cls, diametre):
return cls(diametre / 2)
@staticmethod
def est_valide(rayon):
return rayon > 0
def aire(self):
return Cercle.PI * self.rayon ** 2
c = Cercle.depuis_diametre(10)
print(c.rayon, round(c.aire(), 2), Cercle.est_valide(-1))
L'héritage et super()
Une classe peut hériter d'une autre : class Chien(Animal): donne à Chien tous les attributs et méthodes d'Animal. La classe fille peut redéfinir (*override*) une méthode, et appeler celle de la mère avec super(). Si deux classes filles définissent la même méthode différemment, un même appel a.parler() donne un résultat adapté à chaque objet : c'est le polymorphisme. isinstance(obj, Classe) est vrai pour la classe de l'objet et ses classes mères.
class Animal:
def __init__(self, nom):
self.nom = nom
def parler(self):
return f"{self.nom} fait un bruit."
class Chien(Animal):
def __init__(self, nom, race):
super().__init__(nom)
self.race = race
def parler(self):
return f"{self.nom} aboie !"
class Chat(Animal):
pass
rex = Chien("Rex", "Labrador")
felix = Chat("Felix")
for a in [rex, felix]:
print(a.parler())
print(isinstance(rex, Animal), isinstance(rex, Chat), issubclass(Chien, Animal))
print([c.__name__ for c in Chien.__mro__])
AffichageRex aboie !
Felix fait un bruit.
True False True
['Chien', 'Animal', 'object']
Chien.__mro__ donne l'ordre dans lequel Python cherche une méthode : la classe elle-même, ses mères, puis object, ancêtre de toutes les classes.
Classes abstraites
Une classe peut décrire un contrat sans fournir le code : avec le module abc, une méthode marquée @abstractmethod doit obligatoirement être définie par les classes filles, et on ne peut pas créer d'objet de la classe abstraite elle-même.
from abc import ABC, abstractmethod
class Forme(ABC):
@abstractmethod
def aire(self):
...
class Carre(Forme):
def __init__(self, cote):
self.cote = cote
def aire(self):
return self.cote ** 2
print(Carre(3).aire())
try:
Forme()
except TypeError:
print("Forme est abstraite")
Affichage9
Forme est abstraite
Méthodes spéciales : faire vivre ses objets avec les opérateurs
Les méthodes dont le nom est entouré de deux tirets bas (« dunder » : *double underscore*) sont appelées par Python dans des situations précises : __add__ pour a + b, __eq__ pour a == b, __len__ pour len(a), __getitem__ pour a[i], __lt__ pour a < b… En les définissant, on rend ses objets aussi pratiques que les types intégrés.
class Vecteur:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, autre):
return Vecteur(self.x + autre.x, self.y + autre.y)
def __eq__(self, autre):
return self.x == autre.x and self.y == autre.y
def __len__(self):
return 2
def __getitem__(self, i):
return (self.x, self.y)[i]
def __repr__(self):
return f"Vecteur({self.x}, {self.y})"
u = Vecteur(1, 2)
v = Vecteur(3, 4)
print(u + v, u == Vecteur(1, 2), u == v)
print(len(u), u[1], list(u))
AffichageVecteur(4, 6) True False
2 2 [1, 2]
Définir __lt__ suffit pour que sorted, min et max fonctionnent sur ses objets.
class Eleve:
def __init__(self, nom, note):
self.nom = nom
self.note = note
def __lt__(self, autre):
return self.note < autre.note
def __repr__(self):
return f"{self.nom}({self.note})"
classe = [Eleve("Ada", 18), Eleve("Lin", 15), Eleve("Max", 17)]
print(sorted(classe))
print(max(classe))
Affichage[Lin(15), Max(17), Ada(18)]
Ada(18)
dataclass : une classe de données en trois lignes
Pour une classe qui ne fait que porter des données, le décorateur @dataclass (module dataclasses) écrit à ta place __init__, __repr__ et __eq__ à partir des attributs annotés.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0
p = Point(3)
print(p, p == Point(3, 0), p.x)
AffichagePoint(x=3, y=0) True 3
Composition ou héritage ?
L'héritage exprime « est un » (un Chien est un Animal). Quand un objet contient un autre, on parle de composition (« a un ») : une Voiture a un Moteur. Dans le doute, préfère la composition : elle est plus souple et évite des hiérarchies de classes compliquées.
class Moteur:
def __init__(self, puissance):
self.puissance = puissance
class Voiture:
def __init__(self, marque, puissance):
self.marque = marque
self.moteur = Moteur(puissance)
v = Voiture("Renault", 90)
print(v.marque, v.moteur.puissance)