| name | python-poo |
| description | Programmation Orientée Objet en Python — classes, héritage, MRO, ABC, interfaces, composition vs héritage, encapsulation, SOLID. En français. |
Programmation Orientée Objet Python — Guide Complet (Français)
Ce skill couvre la POO en Python : classes, héritage, MRO, classes abstraites, protocoles, design patterns et principes SOLID.
1. Classes et Objets
Structure de base
from typing import ClassVar
class CompteBancaire:
"""Représente un compte bancaire avec dépôt et retrait.
Attributes:
titulaire: Nom du titulaire du compte.
_solde: Solde actuel (privé, accès via propriété).
_numero: Numéro unique du compte.
"""
_compteur: ClassVar[int] = 0
TAUX_INTERET: ClassVar[float] = 0.02
def __init__(self, titulaire: str, depot_initial: float = 0.0) -> None:
"""Initialise un nouveau compte.
Args:
titulaire: Nom du titulaire.
depot_initial: Montant du dépôt initial.
Raises:
ValueError: Si le dépôt initial est négatif.
"""
if depot_initial < 0:
raise ValueError("Le dépôt initial ne peut pas être négatif")
CompteBancaire._compteur += 1
self.titulaire = titulaire
self._solde = depot_initial
self._numero = CompteBancaire._compteur
def deposer(self, montant: float) -> float:
"""Dépose de l'argent sur le compte.
Args:
montant: Montant à déposer (doit être positif).
Returns:
Le nouveau solde.
"""
if montant <= 0:
raise ValueError("Le montant doit être positif")
self._solde += montant
return self._solde
def retirer(self, montant: float) -> float:
"""Retire de l'argent du compte.
Args:
montant: Montant à retirer.
Returns:
Le nouveau solde.
Raises:
ValueError: Si le solde est insuffisant.
"""
if montant > self._solde:
raise ValueError("Solde insuffisant")
self._solde -= montant
return self._solde
def __str__(self) -> str:
return f"Compte #{self._numero} de {self.titulaire}"
def __repr__(self) -> str:
return f"CompteBancaire('{self.titulaire}', {self._solde})"
def __eq__(self, autre: object) -> bool:
if not isinstance(autre, CompteBancaire):
return NotImplemented
return self._numero == autre._numero
@classmethod
def depuis_csv(cls, ligne: str) -> "CompteBancaire":
"""Crée un compte depuis une ligne CSV.
Args:
ligne: Chaîne au format 'nom,montant'.
Returns:
Une nouvelle instance de CompteBancaire.
"""
nom, montant = ligne.split(",")
return cls(nom.strip(), float(montant))
@staticmethod
def formater_montant(montant: float) -> str:
"""Formate un montant en euros.
Args:
montant: Le montant à formater.
Returns:
Chaîne formatée.
"""
return f"{montant:,.2f} €"
@property
def solde(self) -> float:
"""float: Le solde actuel du compte."""
return self._solde
@property
def numero(self) -> int:
"""int: Le numéro unique du compte (lecture seule)."""
return self._numero
2. Héritage
Héritage simple
class CompteEpargne(CompteBancaire):
"""Compte d'épargne avec taux d'intérêt personnalisé.
Attributes:
taux_interet: Taux d'intérêt annuel du compte.
_retraits_mensuels: Nombre de retraits ce mois-ci.
"""
MAX_RETRAITS_MENSUELS: ClassVar[int] = 3
def __init__(
self,
titulaire: str,
depot_initial: float = 0.0,
taux_interet: float = 0.02,
) -> None:
"""Initialise un compte épargne.
Args:
titulaire: Nom du titulaire.
depot_initial: Dépôt initial.
taux_interet: Taux d'intérêt annuel.
"""
super().__init__(titulaire, depot_initial)
self.taux_interet = taux_interet
self._retraits_mensuels = 0
def retirer(self, montant: float) -> float:
"""Retire de l'argent, limité à MAX_RETRAITS_MENSUELS.
Args:
montant: Montant à retirer.
Returns:
Le nouveau solde.
Raises:
ValueError: Si la limite de retraits est atteinte.
"""
if self._retraits_mensuels >= self.MAX_RETRAITS_MENSUELS:
raise ValueError("Limite de retraits mensuels atteinte")
self._retraits_mensuels += 1
return super().retirer(montant)
def appliquer_interets() -> :
interets = ._solde * (.taux_interet / )
._solde += interets
interets
Héritage multiple et MRO
class Journalisable:
"""Mixin qui ajoute des capacités de journalisation."""
def journaliser(self, message: str) -> None:
"""Enregistre un message dans le journal.
Args:
message: Message à journaliser.
"""
print(f"[{self.__class__.__name__}] {message}")
class Exportable:
"""Mixin qui ajoute des capacités d'export."""
def exporter(self) -> dict[str, Any]:
"""Exporte l'objet en dictionnaire.
Returns:
Dictionnaire représentant l'objet.
"""
return {
k: v for k, v in self.__dict__.items()
if not k.startswith("_")
}
class ComptePremium(CompteEpargne, Journalisable, Exportable):
"""Compte premium avec journalisation et export.
MRO (Method Resolution Order) :
ComptePremium → CompteEpargne → Journalisable → Exportable
→ CompteBancaire → object
"""
def __init__(
self,
titulaire: str,
depot_initial: float = 0.0,
conseiller: str = "",
) -> None:
super().__init__(titulaire, depot_initial, taux_interet=)
.conseiller = conseiller
.journaliser()
(ComptePremium.__mro__)
3. Classes Abstraites (ABC)
from abc import ABC, abstractmethod, abstractproperty
class Forme(ABC):
"""Classe abstraite représentant une forme géométrique.
Toute sous-classe doit implémenter aire() et perimetre().
"""
@abstractmethod
def aire(self) -> float:
"""Calcule l'aire de la forme.
Returns:
L'aire en unités carrées.
"""
...
@abstractmethod
def perimetre(self) -> float:
"""Calcule le périmètre de la forme.
Returns:
Le périmètre en unités.
"""
...
def rapport_aire_perimetre(self) -> float:
"""Calcule le rapport aire/périmètre (méthode concrète).
Returns:
Le rapport, ou infini si le périmètre est nul.
"""
p = self.perimetre()
return self.aire() / p if p != 0 else float("inf")
class Cercle(Forme):
"""Cercle défini par son rayon.
Attributes:
rayon: Rayon du cercle.
"""
def __init__(self, rayon: float) -> None:
"""Initialise le cercle.
Args:
rayon: Rayon du cercle (positif).
"""
.rayon = rayon
() -> :
math
math.pi * .rayon **
() -> :
math
* math.pi * .rayon
():
() -> :
.largeur = largeur
.hauteur = hauteur
() -> :
.largeur * .hauteur
() -> :
* (.largeur + .hauteur)
() -> :
.largeur == .hauteur
4. Composition vs Héritage
class PileHeritee(list):
"""Pile qui hérite de list (mauvaise idée)."""
def empiler(self, element: Any) -> None:
self.append(element)
def depiler(self) -> Any:
return self.pop()
class Pile:
"""Pile FIFO par composition d'une liste."""
def __init__(self) -> None:
self._elements: list[Any] = []
def empiler(self, element: Any) -> None:
"""Ajoute un élément au sommet de la pile."""
self._elements.append(element)
def depiler(self) -> Any:
"""Retire et retourne l'élément au sommet.
Returns:
L'élément retiré.
Raises:
IndexError: Si la pile est vide.
"""
if not self._elements:
raise IndexError("Pile vide")
return ._elements.pop()
() -> :
(._elements)
() -> :
(._elements)
():
() -> :
...
():
() -> :
montant *
():
() -> :
montant *
:
() -> :
.articles = articles
.strategie_taxe = strategie_taxe
() -> :
total_ht = (a[] a .articles)
total_ht + .strategie_taxe.calculer(total_ht)
5. Encapsulation en Python
class AccesDonnees:
"""Démontre les niveaux d'encapsulation en Python."""
def __init__(self) -> None:
self.publique = "Accessible partout"
self._protege = "Accessible, convention interne"
self.__prive = "Name mangling : _AccesDonnees__prive"
def methode_publique(self) -> str:
return "Appelable depuis l'extérieur"
def _methode_protegee(self) -> str:
return "Usage interne, mais techniquement accessible"
def __methode_privee(self) -> str:
"""Vraiment privée via name mangling,
accessible seulement depuis la classe."""
return "Name mangling"
obj = AccesDonnees()
6. Principes SOLID en Python
S — Single Responsibility (Responsabilité Unique)
class RapportUtilisateur:
def generer(self, user): ...
def sauvegarder(self, rapport): ...
def envoyer_email(self, to): ...
class GenerateurRapport:
def generer(self, user) -> str: ...
class StockageRapport:
def sauvegarder(self, rapport: str) -> None: ...
class EnvoyeurEmail:
def envoyer(self, destinataire: str, contenu: str) -> None: ...
O — Open/Closed (Ouvert à l'extension, Fermé à la modification)
class CalculateurRemise:
def calculer(self, commande, type_client: str) -> float:
if type_client == "standard":
return commande.total * 0.05
elif type_client == "premium":
return commande.total * 0.10
elif type_client == "vip":
return commande.total * 0.20
class StrategieRemise(ABC):
@abstractmethod
def appliquer(self, total: float) -> float: ...
class RemiseStandard(StrategieRemise):
def appliquer(self, total: float) -> float:
return total * 0.05
class RemiseVIP(StrategieRemise):
def appliquer(self, total: float) -> float:
return total * 0.20
L — Liskov Substitution
class Rectangle:
def __init__(self, largeur: float, hauteur: float):
self.largeur = largeur
self.hauteur = hauteur
def definir_largeur(self, l: float) -> None:
self.largeur = l
def definir_hauteur(self, h: float) -> None:
self.hauteur = h
class Carre(Rectangle):
def definir_largeur(self, l: float) -> None:
self.largeur = l
self.hauteur = l
def definir_hauteur(self, h: float) -> None:
self.hauteur = h
self.largeur = h
I — Interface Segregation
class Imprimante(ABC):
@abstractmethod
def imprimer(self, doc): ...
@abstractmethod
def scanner(self, doc): ...
@abstractmethod
def faxer(self, doc): ...
class Imprimable(ABC):
@abstractmethod
def imprimer(self, doc): ...
class Scannable(ABC):
@abstractmethod
def scanner(self, doc): ...
class Faxable(ABC):
@abstractmethod
def faxer(self, doc): ...
D — Dependency Inversion
class ServiceNotification:
def __init__(self):
self.smtp = ServeurSMTP()
class ExpediteurMessage(ABC):
@abstractmethod
def envoyer(self, message: str, destinataire: str) -> None: ...
class ExpediteurSMTP(ExpediteurMessage):
def envoyer(self, message: str, destinataire: str) -> None: ...
class ExpediteurSMS(ExpediteurMessage):
def envoyer(self, message: str, destinataire: str) -> None: ...
class ServiceNotification:
def __init__(self, expediteur: ExpediteurMessage) -> None:
self.expediteur = expediteur
7. Design Patterns Courants
Singleton
class ConfigSingleton:
_instance: "ConfigSingleton | None" = None
def __new__(cls) -> "ConfigSingleton":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialiser()
return cls._instance
def _initialiser(self) -> None:
self.debug = False
Factory
class FabriqueForme:
@staticmethod
def creer(type_forme: str, **kwargs) -> Forme:
if type_forme == "cercle":
return Cercle(rayon=kwargs["rayon"])
elif type_forme == "rectangle":
return Rectangle(kwargs["largeur"], kwargs["hauteur"])
raise ValueError(f"Forme inconnue : {type_forme}")
Observer
class Observateur(ABC):
@abstractmethod
def notifier(self, evenement: str, donnees: Any) -> None: ...
class Sujet:
def __init__(self) -> None:
self._observateurs: list[Observateur] = []
def attacher(self, obs: Observateur) -> None:
self._observateurs.append(obs)
def notifier(self, evenement: str, donnees: Any) -> None:
for obs in self._observateurs:
obs.notifier(evenement, donnees)
Références