Python : Les objets (POO)
Classes, self et __init__, héritage, encapsulation par convention, méthodes spéciales et @dataclass : la POO à la sauce Python.
Python : Les objets (POO)
En Python, tout est objet : un int, une chaîne, une fonction, une classe elle-même. La POO y est donc naturelle, mais plus souple et plus « déclarative » qu'en C# : pas de new, pas de private, pas d'interfaces obligatoires. Si tu viens de C#, garde C# : Les objets en tête, les comparaisons ci-dessous s'y réfèrent.
# Définir une classe
class Compte:
def __init__(self, titulaire, solde=0):
self.titulaire = titulaire
self.solde = solde
def deposer(self, montant):
self.solde += montant
compte = Compte("Alice", 100) # pas de new
compte.deposer(50)
compte.solde # 150class Compte: def __init__(self, titulaire, solde=0): self.titulaire = titulaire self.solde = solde def deposer(self, montant): self.solde += montant compte = Compte("Alice", 100) # pas de newcompte.deposer(50)compte.solde # 150__init__joue le rôle du constructeur : il initialise l'objet déjà créé.selfdésigne l'instance courante. C'est l'équivalent duthisde C#, à une différence près : il est explicite, on l'écrit en premier paramètre de chaque méthode.- Les attributs ne se déclarent pas à l'avance : ils naissent à la première affectation
self.x = valeurdans__init__.
compte.deposer(50) n'est qu'un raccourci pour Compte.deposer(compte, 50). C'est pour ça que self est explicite : c'est littéralement l'objet passé en premier argument.
Et puisque c'est un paramètre comme un autre, self n'est pas un mot-clé du langage, juste une convention. Le nom est libre :
class Compte:
def deposer(moi, montant): # fonctionne exactement pareil
moi.solde += montantclass Compte: def deposer(moi, montant): # fonctionne exactement pareil moi.solde += montant Possible, mais à éviter : la convention est universelle (PEP 8 la recommande) et tout lecteur, linter ou IDE s'attend à self. Même chose pour cls, le nom d'usage du premier paramètre d'une @classmethod, qui reçoit la classe au lieu de l'instance.
# Attributs d'instance et attributs de classe
Un attribut défini dans le corps de la classe (hors méthode) est partagé par toutes les instances, comme un static en C#.
class Compte:
taux_interet = 0.02 # attribut de classe : partagé
def __init__(self, titulaire):
self.titulaire = titulaire # attribut d'instance : propre à chaque objet
Compte.taux_interet # 0.02, accessible sans instanceclass Compte: taux_interet = 0.02 # attribut de classe : partagé def __init__(self, titulaire): self.titulaire = titulaire # attribut d'instance : propre à chaque objet Compte.taux_interet # 0.02, accessible sans instance Même piège que la valeur par défaut modifiable : un attribut de classe modifiable (articles = []) est partagé entre toutes les instances. Ajouter un article à un panier le fait apparaître dans tous. Les données propres à chaque objet s'initialisent dans __init__.
# Pas de private : l'encapsulation par convention
Python n'a ni private ni protected. La philosophie : « nous sommes entre adultes consentants ». On signale l'intention par le nommage :
| Nommage | Signification |
|---|---|
solde | Public. |
_solde | « Interne, n'y touche pas ». Simple convention, rien ne l'empêche. |
__solde | Renommé en _Compte__solde (name mangling) pour éviter les collisions en héritage. Pas une vraie protection non plus. |
C'est une des facettes de la permissivité du langage : l'encapsulation repose sur la discipline de l'équipe, pas sur le compilateur.
# Les propriétés (@property)
Pour contrôler l'accès à un attribut sans changer la syntaxe d'appel, on utilise @property, l'équivalent des propriétés get / set de C#.
class Compte:
def __init__(self, solde):
self._solde = solde
@property
def solde(self): # lecture : compte.solde
return self._solde
@solde.setter
def solde(self, valeur): # écriture : compte.solde = 10
if valeur < 0:
raise ValueError("Le solde ne peut pas être négatif")
self._solde = valeurclass Compte: def __init__(self, solde): self._solde = solde @property def solde(self): # lecture : compte.solde return self._solde @solde.setter def solde(self, valeur): # écriture : compte.solde = 10 if valeur < 0: raise ValueError("Le solde ne peut pas être négatif") self._solde = valeurL'appelant écrit toujours compte.solde, comme pour un attribut simple. On peut donc commencer avec un attribut public et le transformer en propriété plus tard sans casser le code existant. C'est pour ça qu'en Python, on n'écrit pas de getters / setters « par précaution ».
# L'héritage
La classe parente se passe entre parenthèses. super() donne accès à ses méthodes.
class Animal:
def __init__(self, nom):
self.nom = nom
def parler(self):
return "Bruit générique"
class Chien(Animal):
def __init__(self, nom, race):
super().__init__(nom) # appelle le __init__ d'Animal
self.race = race
def parler(self): # redéfinition : pas de virtual / override
return "Wouf"
rex = Chien("Rex", "berger")
rex.parler() # "Wouf"
isinstance(rex, Animal) # Trueclass Animal: def __init__(self, nom): self.nom = nom def parler(self): return "Bruit générique" class Chien(Animal): def __init__(self, nom, race): super().__init__(nom) # appelle le __init__ d'Animal self.race = race def parler(self): # redéfinition : pas de virtual / override return "Wouf" rex = Chien("Rex", "berger")rex.parler() # "Wouf"isinstance(rex, Animal) # TrueToutes les méthodes sont redéfinissables, sans mot-clé virtual ni override. Python autorise aussi l'héritage multiple (class C(A, B)), là où C# se limite à une classe parente plus des interfaces. Puissant, mais à manier avec parcimonie.
# Pas besoin d'interface : le duck typing
En C#, pour qu'une méthode accepte « tout ce qui sait parler », on déclare une interface. En Python, on n'en a pas besoin : si l'objet a une méthode parler(), on peut l'appeler. C'est le duck typing : « si ça cancane comme un canard, c'est un canard ».
class Robot: # n'hérite pas d'Animal
def parler(self):
return "Bip"
for chose in [Chien("Rex", "berger"), Robot()]:
print(chose.parler()) # "Wouf", puis "Bip"class Robot: # n'hérite pas d'Animal def parler(self): return "Bip" for chose in [Chien("Rex", "berger"), Robot()]: print(chose.parler()) # "Wouf", puis "Bip"Quand on veut quand même imposer un contrat, le module abc fournit les classes abstraites, pendant des classes abstraites de C# :
from abc import ABC, abstractmethod
class Forme(ABC):
@abstractmethod
def aire(self):
pass
Forme() # TypeError : impossible d'instancier une classe abstraitefrom abc import ABC, abstractmethod class Forme(ABC): @abstractmethod def aire(self): pass Forme() # TypeError : impossible d'instancier une classe abstraite# Les méthodes spéciales (dunder)
Les méthodes entourées de doubles underscores (dunder, pour double underscore) branchent ta classe sur la syntaxe du langage. C'est ce qui rend les objets Python si « natifs ».
| Méthode | Déclenchée par | Rôle |
|---|---|---|
__init__ | Compte(...) | Initialisation |
__str__ | print(obj), str(obj) | Affichage lisible pour l'utilisateur |
__repr__ | repr(obj), la console, une liste d'objets | Représentation technique, pour le debug |
__eq__ | a == b | Égalité (par défaut : même objet en mémoire) |
__len__ | len(obj) | Taille |
__add__ | a + b | Surcharge d'opérateur |
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point({self.x}, {self.y})"
def __eq__(self, autre):
return (self.x, self.y) == (autre.x, autre.y)
Point(1, 2) == Point(1, 2) # True grâce à __eq__class Point: def __init__(self, x, y): self.x, self.y = x, y def __repr__(self): return f"Point({self.x}, {self.y})" def __eq__(self, autre): return (self.x, self.y) == (autre.x, autre.y) Point(1, 2) == Point(1, 2) # True grâce à __eq__ Sans __eq__, deux objets aux mêmes valeurs sont différents : Python compare les références, exactement comme une classe C#. Même logique que dans HashSet et l'égalité.
# @dataclass : le record de Python
Écrire __init__, __repr__ et __eq__ à la main pour une simple classe de données est fastidieux. Le décorateur @dataclass les génère à partir des champs annotés, comme un record en C#.
from dataclasses import dataclass, field
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
p # Point(x=1, y=2) (__repr__ généré)
p == Point(1, 2) # True (__eq__ généré)
@dataclass(frozen=True) # immuable, comme un record
class Coordonnee:
lat: float
lon: float
@dataclass
class Panier:
articles: list = field(default_factory=list) # une liste neuve par instancefrom dataclasses import dataclass, field @dataclassclass Point: x: int y: int p = Point(1, 2)p # Point(x=1, y=2) (__repr__ généré)p == Point(1, 2) # True (__eq__ généré) @dataclass(frozen=True) # immuable, comme un recordclass Coordonnee: lat: float lon: float @dataclassclass Panier: articles: list = field(default_factory=list) # une liste neuve par instancefrozen=Trueinterdit toute modification après création, et rend l'objet utilisable comme clé dedictou élément deset.field(default_factory=list)règle proprement le piège de la liste partagée vu plus haut.
Les annotations (x: int) ne sont pas vérifiées à l'exécution : Point("a", "b") passe sans erreur. Elles servent à @dataclass pour connaître les champs, et aux outils comme mypy pour la vérification statique.
# La suite
- Les exceptions - gérer les erreurs, et créer les tiennes en héritant d'
Exception - Les fonctions - les méthodes ne sont que des fonctions attachées à une classe
- C# : Les objets - la même chose, côté typage statique
- Python : Introduction - le hub de la série