■ PYTHON
Programmation Orientée Objet
Cours Complet — du débutant à l'expert
Niveau Débutant → Avancé
Chapitres 10 chapitres complets
Contenu Théorie + Exemples + Exercices
Version Python 3.10+
Ce cours vous guide pas à pas dans la maîtrise de la Programmation Orientée Objet avec
Python. Chaque chapitre combine théorie claire, exemples pratiques et exercices pour
consolider vos acquis.
Chapitre — Table des Matières
Chapitre 1 — Introduction à la POO
› Paradigmes de programmation
› Concepts fondamentaux
› Avantages de la POO
Chapitre 2 — Classes et Objets
› Définir une classe
› Attributs et méthodes
› Instanciation
Chapitre 3 — Constructeurs et Attributs
› __init__
› Attributs d'instance vs de classe
› Valeurs par défaut
Chapitre 4 — Encapsulation
› Principe d'encapsulation
› Attributs privés
› Getters & Setters
› Properties
Chapitre 5 — Héritage
› Héritage simple
› Héritage multiple
› super()
› MRO
Chapitre 6 — Polymorphisme
› Duck typing
› Surcharge d'opérateurs
› Méthodes spéciales
Chapitre 7 — Abstraction
› Classes abstraites (ABC)
› Interfaces en Python
› Méthodes abstraites
Chapitre 8 — Méthodes et Attributs Spéciaux
› Méthodes dunder
› Représentation
› Comparaison
› Conteneurs
Chapitre 9 — Patrons de Conception (Design Patterns)
› Singleton
› Factory
› Observer
› Decorator
Chapitre 10 — Projet Pratique Complet
› Système de gestion
› Application des concepts
› Bonnes pratiques
Chapitre 1 — Introduction à la POO
■ Qu'est-ce que la Programmation Orientée Objet ?
La Programmation Orientée Objet (POO) est un paradigme de programmation qui organise le code
autour d'objets plutôt qu'autour de fonctions et de procédures. Un objet est une entité qui regroupe des
données (attributs) et des comportements (méthodes).
■ Les 4 paradigmes principaux
Paradigme Description Exemple
Procédural Séquence d'instructions C, Pascal
Fonctionnel Fonctions sans état Haskell, Erlang
Orienté Objet Objets avec état Python, Java, C++
Déclaratif Décrire le résultat SQL, HTML
■ Les 4 piliers de la POO
Encapsulation Héritage Polymorphisme Abstraction
Regrouper données et Une classe peut hériter Un même message peut Simplifier la complexité
méthodes, cacher les des attributs/méthodes produire des en exposant uniquement
détails internes. d'une autre. comportements l'essentiel.
différents.
■ Premier exemple : classe vs procédural
◆ Approche procédurale
# Approche procédurale
nom = 'Alice'
age = 30
salaire = 3000
def afficher_employe(nom, age, salaire):
print(f'Employé : {nom}, {age} ans, {salaire}€/mois')
afficher_employe(nom, age, salaire)
◆ Approche orientée objet
# Approche orientée objet
class Employe:
def __init__(self, nom, age, salaire):
[Link] = nom
[Link] = age
[Link] = salaire
def afficher(self):
print(f'Employé : {[Link]}, {[Link]} ans, {[Link]}€/mois')
alice = Employe('Alice', 30, 3000)
[Link]() # Employé : Alice, 30 ans, 3000€/mois
■ Note : La POO permet de regrouper les données et les comportements liés dans une même entité logique.
Chapitre 2 — Classes et Objets
■ Définir une classe
Une classe est un modèle (blueprint) qui décrit la structure et le comportement d'un type d'objet. Les
objets sont des instances concrètes de cette classe.
class NomClasse:
# Corps de la classe
attribut_de_classe = valeur
def methode(self):
pass
■ Exemple complet : classe Voiture
class Voiture:
# Attribut de classe (partagé par toutes les instances)
nombre_de_roues = 4
def __init__(self, marque, modele, annee):
# Attributs d'instance
[Link] = marque
[Link] = modele
[Link] = annee
[Link] = 0
def accelerer(self, increment):
[Link] += increment
print(f'{[Link]} accélère à {[Link]} km/h')
def freiner(self, decrement):
[Link] = max(0, [Link] - decrement)
def afficher_infos(self):
print(f'{[Link]} {[Link]} ({[Link]})')
print(f'Vitesse actuelle : {[Link]} km/h')
# Instanciation
ma_voiture = Voiture('Renault', 'Clio', 2022)
ma_voiture.accelerer(50) # Renault accélère à 50 km/h
ma_voiture.afficher_infos()
print(Voiture.nombre_de_roues) # 4 — attribut de classe
■ Objets et identité
Chaque objet créé est une entité distincte en mémoire. Deux objets issus de la même classe sont
indépendants.
v1 = Voiture('Peugeot', '208', 2021)
v2 = Voiture('Toyota', 'Yaris', 2023)
[Link](30)
print([Link]) # 0 — v2 n'est pas affecté par v1
print(v1 is v2) # False — objets distincts
print(id(v1), id(v2)) # adresses mémoire différentes
■ Note : self fait toujours référence à l'objet courant. C'est la convention universelle en Python.
Chapitre 3 — Constructeurs et Attributs
■ Le constructeur __init__
Le constructeur __init__ est une méthode spéciale appelée automatiquement lors de la création d'un
objet. Il initialise les attributs de l'instance.
class Personne:
def __init__(self, nom, age, ville='Paris'): # valeur par défaut
[Link] = nom
[Link] = age
[Link] = ville
p1 = Personne('Alice', 30) # ville = 'Paris' par défaut
p2 = Personne('Bob', 25, 'Lyon') # ville = 'Lyon'
print([Link]) # Paris
print([Link]) # Lyon
■ Attributs d'instance vs attributs de classe
class Compteur:
total = 0 # Attribut de CLASSE — partagé
def __init__(self, nom):
[Link] = nom # Attribut d'INSTANCE — propre à chaque objet
[Link] += 1
@classmethod
def get_total(cls):
return [Link]
c1 = Compteur('A')
c2 = Compteur('B')
c3 = Compteur('C')
print(Compteur.get_total()) # 3
■ Méthodes statiques et de classe
class Utilitaire:
_instances = 0
def __init__(self):
Utilitaire._instances += 1
@staticmethod
def convertir_celsius_fahrenheit(c):
# Pas besoin de self ou cls
return c * 9/5 + 32
@classmethod
def nombre_instances(cls):
return cls._instances
print(Utilitaire.convertir_celsius_fahrenheit(100)) # 212.0
u1 = Utilitaire()
u2 = Utilitaire()
print(Utilitaire.nombre_instances()) # 2
■ Note : @staticmethod n'a pas accès à la classe ni à l'instance. @classmethod reçoit cls (la classe) comme
premier argument.
Chapitre 4 — Encapsulation
■ Principe d'encapsulation
L'encapsulation consiste à restreindre l'accès direct aux attributs internes d'un objet pour protéger son
état et garantir la cohérence des données. Elle distingue l'interface publique de l'implémentation interne.
■ Niveaux d'accès en Python
Convention Syntaxe Signification
Public attribut Accessible partout
Protégé _attribut Convention : usage interne (non strict)
Privé __attribut Name mangling — difficile d'accéder hors classe
■ Exemple : attribut privé avec getters/setters
class CompteBancaire:
def __init__(self, titulaire, solde_initial=0):
[Link] = titulaire
self.__solde = solde_initial # attribut privé
def deposer(self, montant):
if montant > 0:
self.__solde += montant
print(f'+{montant}€ déposés')
def retirer(self, montant):
if 0 < montant <= self.__solde:
self.__solde -= montant
print(f'-{montant}€ retirés')
else:
print('Solde insuffisant ou montant invalide')
def get_solde(self):
return self.__solde
compte = CompteBancaire('Alice', 500)
[Link](200)
[Link](100)
print(compte.get_solde()) # 600
# print(compte.__solde) # AttributeError !
■ Properties : la façon Pythonique
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@[Link]
def celsius(self, valeur):
if valeur < -273.15:
raise ValueError('Température impossible (< zéro absolu)')
self._celsius = valeur
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
t = Temperature(25)
print([Link]) # 77.0
[Link] = 100
print([Link]) # 212.0
[Link] = -300 # ValueError
■ Note : Les @property permettent d'accéder aux attributs comme s'ils étaient publics tout en contrôlant les
accès.
Chapitre 5 — Héritage
■ Principe de l'héritage
L'héritage permet à une classe enfant (sous-classe) d'hériter des attributs et méthodes d'une classe
parente (super-classe). Cela favorise la réutilisation du code et la hiérarchie logique entre les objets.
class Animal:
def __init__(self, nom, age):
[Link] = nom
[Link] = age
def respirer(self):
print(f'{[Link]} respire...')
def se_presenter(self):
print(f'Je suis {[Link]}, {[Link]} ans')
class Chien(Animal): # Chien hérite de Animal
def __init__(self, nom, age, race):
super().__init__(nom, age) # appel constructeur parent
[Link] = race
def aboyer(self):
print(f'{[Link]} : Woof !')
rex = Chien('Rex', 5, 'Labrador')
rex.se_presenter() # méthode héritée
[Link]() # méthode héritée
[Link]() # méthode propre à Chien
■ Surcharge de méthodes (Override)
class Chat(Animal):
def __init__(self, nom, age, interieur=True):
super().__init__(nom, age)
[Link] = interieur
def se_presenter(self): # SURCHARGE
super().se_presenter() # appel de la version parente
print(f'Je suis un chat {'d\'intérieur' if [Link] else 'd\'extérieur'}')
def ronronner(self):
print(f'{[Link]} : Purr...')
minou = Chat('Minou', 3)
minou.se_presenter()
# Je suis Minou, 3 ans
# Je suis un chat d'intérieur
■ Héritage multiple
class Nageur:
def nager(self):
print('Je nage')
class Coureur:
def courir(self):
print('Je cours')
class Triathlète(Nageur, Coureur):
def s_entrainer(self):
[Link]()
[Link]()
t = Triathlète()
t.s_entrainer()
■ MRO — Ordre de Résolution des Méthodes
# Python utilise l'algorithme C3 pour résoudre l'héritage multiple
print(Triathlète.__mro__)
# (<class 'Triathlète'>, <class 'Nageur'>, <class 'Coureur'>, <class 'object'>)
# isinstance et issubclass
print(isinstance(rex, Animal)) # True
print(isinstance(rex, Chien)) # True
print(issubclass(Chien, Animal)) # True
■ Note : Toute classe Python hérite implicitement de object, la classe racine universelle.
Chapitre 6 — Polymorphisme
■ Qu'est-ce que le polymorphisme ?
Le polymorphisme (du grec : plusieurs formes) permet à des objets de types différents de répondre de
manière appropriée à un même message (appel de méthode). En Python, il est naturel grâce au duck
typing.
■ Duck Typing
"Si ça marche comme un canard et que ça cancane comme un canard, c'est un canard."
class Chien:
def parler(self):
return 'Woof!'
class Chat:
def parler(self):
return 'Miaou!'
class Perroquet:
def parler(self):
return 'Coucou !'
animaux = [Chien(), Chat(), Perroquet()]
for animal in animaux:
print([Link]()) # chaque objet répond à sa façon
# Woof!
# Miaou!
# Coucou !
■ Surcharge d'opérateurs
Python permet de redéfinir le comportement des opérateurs (+, -, *, ==…) via les méthodes spéciales.
class Vecteur:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, autre): # surcharge de +
return Vecteur(self.x + autre.x, self.y + autre.y)
def __mul__(self, scalaire): # surcharge de *
return Vecteur(self.x * scalaire, self.y * scalaire)
def __eq__(self, autre): # surcharge de ==
return self.x == autre.x and self.y == autre.y
def __repr__(self):
return f'Vecteur({self.x}, {self.y})'
v1 = Vecteur(1, 2)
v2 = Vecteur(3, 4)
print(v1 + v2) # Vecteur(4, 6)
print(v1 * 3) # Vecteur(3, 6)
print(v1 == v2) # False
■ Note : La surcharge d'opérateurs rend vos objets plus intuitifs et expressifs à utiliser.
Chapitre 7 — Abstraction
■ Concept d'abstraction
L'abstraction consiste à définir une interface commune sans spécifier l'implémentation. En Python, on
utilise le module abc (Abstract Base Classes) pour créer des classes abstraites qui imposent un contrat
aux sous-classes.
■ Classes abstraites avec ABC
from abc import ABC, abstractmethod
class Forme(ABC): # Classe abstraite
@abstractmethod
def aire(self) -> float:
pass
@abstractmethod
def perimetre(self) -> float:
pass
def description(self):
# Méthode concrète utilisable par toutes les formes
print(f'Aire = {[Link]():.2f}, Périmètre = {[Link]():.2f}')
# form = Forme() # TypeError : on ne peut pas instancier une classe abstraite
class Cercle(Forme):
def __init__(self, rayon):
[Link] = rayon
def aire(self):
import math
return [Link] * [Link] ** 2
def perimetre(self):
import math
return 2 * [Link] * [Link]
class Rectangle(Forme):
def __init__(self, largeur, hauteur):
[Link] = largeur
[Link] = hauteur
def aire(self):
return [Link] * [Link]
def perimetre(self):
return 2 * ([Link] + [Link])
formes = [Cercle(5), Rectangle(4, 6)]
for f in formes:
[Link]()
# Aire = 78.54, Périmètre = 31.42
# Aire = 24.00, Périmètre = 20.00
■ Protocoles (Python 3.8+)
from typing import Protocol
class Dessinable(Protocol):
def dessiner(self) -> None: ...
class Carré:
def dessiner(self):
print('■ Dessin d\'un carré')
class Triangle:
def dessiner(self):
print('■ Dessin d\'un triangle')
def afficher(forme: Dessinable):
[Link]()
afficher(Carré()) # ■ Dessin d'un carré
afficher(Triangle()) # ■ Dessin d'un triangle
■ Note : Les Protocols permettent le duck typing statiquement vérifié sans héritage explicite.
Chapitre 8 — Méthodes et Attributs Spéciaux
(Dunder)
■ Qu'est-ce que les méthodes dunder ?
Les méthodes dunder (double underscore) comme __init__, __str__, __len__… permettent à vos
classes de s'intégrer naturellement avec la syntaxe Python et ses fonctions intégrées.
Méthode Déclenchée par Usage
__init__ ClassName() Constructeur
__str__ str(obj), print(obj) Représentation lisible
__repr__ repr(obj) Représentation de débogage
__len__ len(obj) Longueur
__getitem__ obj[clé] Accès par index/clé
__setitem__ obj[clé] = val Affectation par index
__contains__ x in obj Test d'appartenance
__iter__ for x in obj Itération
__eq__ obj1 == obj2 Égalité
__lt__ obj1 < obj2 Inférieur à
__add__ obj1 + obj2 Addition
__enter__/__exit__ with obj as x Context manager
■ Exemple : classe Liste personnalisée
class MaListe:
def __init__(self, *args):
self._data = list(args)
def __len__(self):
return len(self._data)
def __getitem__(self, index):
return self._data[index]
def __setitem__(self, index, valeur):
self._data[index] = valeur
def __contains__(self, item):
return item in self._data
def __iter__(self):
return iter(self._data)
def __str__(self):
return f'MaListe({self._data})'
def __repr__(self):
return f'MaListe(*{self._data!r})'
ml = MaListe(10, 20, 30, 40)
print(len(ml)) # 4
print(ml[1]) # 20
print(30 in ml) # True
for v in ml: print(v, end=' ') # 10 20 30 40
print(str(ml)) # MaListe([10, 20, 30, 40])
■ Note : Implémenter les méthodes dunder rend vos objets 'pythoniques' et compatibles avec les idiomes du
langage.
Chapitre 9 — Patrons de Conception (Design
Patterns)
■ Introduction aux Design Patterns
Les patrons de conception sont des solutions éprouvées à des problèmes récurrents de conception
logicielle. Ils se classent en trois familles : créationnels, structurels et comportementaux.
◆ 1. Singleton — une seule instance
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, valeur=None):
if not hasattr(self, 'initialise'):
[Link] = valeur
[Link] = True
s1 = Singleton(42)
s2 = Singleton(99)
print(s1 is s2) # True — même objet !
print([Link]) # 42
◆ 2. Factory — créer des objets sans spécifier leur classe
class Animal:
def parler(self): pass
class Chien(Animal):
def parler(self): return 'Woof!'
class Chat(Animal):
def parler(self): return 'Miaou!'
class AnimalFactory:
_registry = {'chien': Chien, 'chat': Chat}
@classmethod
def creer(cls, type_animal: str) -> Animal:
klass = cls._registry.get(type_animal.lower())
if klass is None:
raise ValueError(f'Inconnu : {type_animal}')
return klass()
a = [Link]('chien')
print([Link]()) # Woof!
◆ 3. Observer — notification d'événements
class Evenement:
def __init__(self):
self._abonnes = []
def abonner(self, callback):
self._abonnes.append(callback)
def emettre(self, *args, **kwargs):
for cb in self._abonnes:
cb(*args, **kwargs)
class Capteur:
def __init__(self):
self.on_mesure = Evenement()
self._valeur = 0
def mesurer(self, valeur):
self._valeur = valeur
self.on_mesure.emettre(valeur)
capteur = Capteur()
capteur.on_mesure.abonner(lambda v: print(f'Alerte ! valeur = {v}'))
capteur.on_mesure.abonner(lambda v: print(f'Log : {v}'))
[Link](42)
# Alerte ! valeur = 42
# Log : 42
Chapitre 10 — Projet Pratique Complet
■ Système de gestion d'une bibliothèque
Ce projet applique tous les concepts vus : classes, héritage, encapsulation, polymorphisme, méthodes
spéciales et design patterns.
◆ Modèle de données
from abc import ABC, abstractmethod
from datetime import date, timedelta
# ■■ Document (classe abstraite) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class Document(ABC):
def __init__(self, titre, auteur, annee):
[Link] = titre
[Link] = auteur
[Link] = annee
self._emprunte = False
@property
def emprunte(self):
return self._emprunte
@abstractmethod
def type_document(self) -> str:
pass
def __str__(self):
statut = 'emprunté' if self._emprunte else 'disponible'
return f'[{self.type_document()}] {[Link]} ({[Link]}, {[Link]}) —
{statut}'
def __repr__(self):
return f'{self.__class__.__name__}({[Link]!r})'
# ■■ Sous-classes ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class Livre(Document):
def __init__(self, titre, auteur, annee, isbn):
super().__init__(titre, auteur, annee)
[Link] = isbn
def type_document(self):
return 'Livre'
class Revue(Document):
def __init__(self, titre, auteur, annee, numero):
super().__init__(titre, auteur, annee)
[Link] = numero
def type_document(self):
return 'Revue'
# ■■ Emprunt ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class Emprunt:
DUREE_DEFAUT = 14 # jours
def __init__(self, document, adherent):
[Link] = document
[Link] = adherent
self.date_emprunt = [Link]()
self.date_retour = self.date_emprunt + timedelta(days=self.DUREE_DEFAUT)
document._emprunte = True
def retourner(self):
[Link]._emprunte = False
print(f'{[Link]} retourné par {[Link]}')
def est_en_retard(self):
return [Link]() > self.date_retour
def __str__(self):
retard = ' ■ EN RETARD' if self.est_en_retard() else ''
return (f'{[Link]} — {[Link]} | '
f'retour prévu : {self.date_retour}{retard}')
# ■■ Bibliothèque (Façade + gestion) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class Bibliotheque:
def __init__(self, nom):
[Link] = nom
self._catalogue: list[Document] = []
self._emprunts: list[Emprunt] = []
def ajouter(self, document: Document):
self._catalogue.append(document)
print(f'Ajout : {document}')
def rechercher(self, mot_cle: str) -> list[Document]:
mc = mot_cle.lower()
return [d for d in self._catalogue
if mc in [Link]() or mc in [Link]()]
def emprunter(self, titre: str, adherent: str):
for d in self._catalogue:
if [Link]() == [Link]():
if [Link]:
print(f'{titre} est déjà emprunté')
return None
emp = Emprunt(d, adherent)
self._emprunts.append(emp)
print(f'Emprunt : {emp}')
return emp
print(f'Document non trouvé : {titre}')
return None
def rapport(self):
print(f'\n=== {[Link]} — {len(self._catalogue)} document(s) ===')
for d in self._catalogue:
print(' ', d)
# ■■ Démonstration ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
bib = Bibliotheque('Médiathèque Centrale')
[Link](Livre('Le Petit Prince', 'Saint-Exupéry', 1943, '978-2-07-040850-4'))
[Link](Revue('Science & Vie', 'Collectif', 2024, 1298))
[Link](Livre('Clean Code', 'Robert C. Martin', 2008, '978-0-13-235088-4'))
[Link]('Clean Code', 'Alice')
[Link]('Clean Code', 'Bob') # déjà emprunté
resultats = [Link]('prince')
print('Résultats :', resultats)
[Link]()
■ Bonnes pratiques POO
• Principe de responsabilité unique (SRP) : une classe = une responsabilité.
• Préférez la composition à l'héritage quand c'est possible.
• Utilisez les @property plutôt que les getters/setters explicites.
• Documentez vos classes avec des docstrings.
• Utilisez les type hints pour clarifier les signatures.
• Écrivez des tests unitaires pour chaque classe.
• Respectez le principe SOLID pour un code maintenable.
■ Félicitations !
Vous avez parcouru l'ensemble du cours Python POO. Vous maîtrisez maintenant les classes, l'héritage,
l'encapsulation, le polymorphisme, l'abstraction, les méthodes spéciales et les design patterns. Pratiquez
régulièrement en construisant vos propres projets !