Python — programmation orientée objet
Regrouper les données et les traitements qui vont ensemble, au lieu de les laisser éparpillés.
Résumé flash
La POO réunit dans une même structure les données
(attributs) et les actions (méthodes). Une classe est le
plan ; un objet en est une réalisation. __init__ construit
l'objet, self le désigne. L'encapsulation protège les données
derrière des contrôles, et l'héritage évite de réécrire ce qui est
commun.
🔑
#class #objet #instance #self
#__init__ #__del__ #attribut #méthode
#encapsulation #getter #setter #@property
#héritage #override #super
01Pourquoi la POO
Dans les premiers programmes, les données et les fonctions sont séparées. Quand l'application grandit, le code devient difficile à organiser, à maintenir et à réutiliser. La POO apporte une solution : regrouper les données et les traitements associés dans une même structure, l'objet.
- organiser le code de façon claire ;
- faciliter la réutilisation ;
- simplifier la maintenance ;
- représenter des entités du monde réel.
Une voiture possède des caractéristiques (marque, modèle, couleur, vitesse) et des actions (démarrer, accélérer, freiner, arrêter). En POO :
| les caractéristiques | deviennent des attributs |
| les actions | deviennent des méthodes |
| la voiture | devient un objet |
| le modèle général servant à en créer plusieurs | devient une classe |
02Classe et objet
Une classe est un modèle, un plan. Un objet (ou instance) est une réalisation concrète de ce plan.
class NomDeLaClasse:
attribut_classe = valeur # attribut de CLASSE (partagé)
def __init__(self, parametres): # constructeur
self.attribut_instance = parametres # attribut d'INSTANCE (propre)
def methode(self): # méthode
passclass Personne:
def __init__(self, nom, age):
self.nom = nom
self.age = age
p1 = Personne("Alice", 30) # instanciation
p2 = Personne("Bob", 25)
print(p1.nom) # Alice
print(p2.nom) # Bob ← chaque objet a SES valeursConvention Python (la PEP 8) : les classes s'écrivent en
CamelCase — Personne, Equipement,
ServeurWeb — tandis que variables et fonctions restent en minuscules avec des
soulignés. À la lecture, une majuscule signale immédiatement une classe.
03Attributs, self et __init__
self : l'objet lui-même
self représente l'instance courante — l'objet sur lequel la
méthode est appelée. Il permet d'accéder à ses attributs et à ses méthodes.
class Voiture:
def afficher_marque(self):
print(self.marque) # self.marque = « la marque DE CET objet »__init__ : le constructeur
Méthode appelée automatiquement à la création d'un objet. Elle sert à initialiser les attributs.
class Equipement:
def __init__(self, ip, nom):
self.ip = ip
self.nom = nom
sw = Equipement("192.168.1.10", "SW-01")
# ↑ ↑
# ces valeurs partent dans __init__self s'écrit, mais ne se passe jamaisclass Equipement:
def afficher(self): # ← self est DÉCLARÉ
print(self.ip)
sw.afficher() # ← mais on ne le PASSE pas
sw.afficher(sw) # ✘ TypeError: takes 1 positional argument but 2 were givenPython le transmet tout seul : sw.afficher() devient en coulisses
Equipement.afficher(sw).
L'erreur inverse est encore plus fréquente — oublier self dans
la définition :
def afficher(): # ✘ self manquant
print(self.ip)
# TypeError: afficher() takes 0 positional arguments but 1 was givenLe message paraît absurde (« 0 attendu, 1 reçu ») : c'est justement le self
automatique qui arrive et que la méthode n'attend pas.
Attribut de classe ou d'instance ?
class Equipement:
nombre = 0 # attribut de CLASSE : partagé par tous
def __init__(self, ip):
self.ip = ip # attribut d'INSTANCE : propre à chacun
Equipement.nombre += 1
a = Equipement("192.168.1.1")
b = Equipement("192.168.1.2")
print(a.ip, b.ip) # 192.168.1.1 192.168.1.2 ← différents
print(Equipement.nombre) # 2 ← communInstance : tout ce qui varie d'un objet à l'autre — l'IP, le nom, l'état. Classe : ce qui vaut pour tous — un compteur, une constante, une valeur par défaut commune.
En pratique, 90 % des attributs sont des attributs d'instance, définis dans
__init__.
04Les méthodes
Une méthode est une fonction définie dans une classe : c'est une action que l'objet sait faire.
class Voiture:
def __init__(self, marque):
self.marque = marque
self.vitesse = 0
def accelerer(self, valeur):
self.vitesse += valeur
def freiner(self):
self.vitesse = 0
def afficher(self):
print(f"{self.marque} roule à {self.vitesse} km/h")
v = Voiture("Renault")
v.accelerer(50)
v.accelerer(30)
v.afficher() # Renault roule à 80 km/h
v.freiner()
v.afficher() # Renault roule à 0 km/hSans POO, on écrirait vitesse = vitesse + 50 quelque part dans le programme.
Avec, on dit v.accelerer(50) : c'est la voiture qui gère sa
vitesse.
La différence devient décisive le jour où accélérer implique aussi de consommer du carburant et de vérifier une limite : tout est dans la méthode, et tous les appels en profitent.
05Le destructeur
Le destructeur est appelé quand l'objet est détruit — utile pour libérer une ressource, fermer un fichier ou une connexion.
class Connexion:
def __init__(self, serveur):
self.serveur = serveur
print(f"Connexion à {serveur} ouverte")
def __del__(self):
print(f"Connexion à {self.serveur} fermée")
c = Connexion("192.168.1.1")
del c # Connexion à 192.168.1.1 fermée__del__, pas
__def__Le sommaire du polycopié annonce « Un destructeur (__def__) ». C'est une
coquille : la méthode correcte est __del__ (comme
delete), et le cours l'écrit d'ailleurs correctement plus loin, à la section 8.
__def__ n'existe pas en Python : définie ainsi, la méthode ne serait
jamais appelée, sans le moindre message d'erreur — le genre de bug qu'on
cherche longtemps.
06L'encapsulation
Considérons cette classe, sans protection :
class Personne:
def __init__(self, nom, age):
self.nom = nom
self.age = age
p1 = Personne("Alice", 30)
p1.age = -10 # ← rien ne l'empêche
print(p1.age) # -10L'attribut age est accessible directement : n'importe quelle valeur
peut lui être attribuée, même incohérente. L'encapsulation apporte la solution — empêcher
l'accès direct et imposer des contrôles avant toute modification. L'objet
conserve alors un état valide tout au long de sa vie.
Les trois niveaux, par convention
Contrairement à Java ou C++, Python n'a pas les mots-clés
public, protected, private. Il repose sur une
convention de nommage.
| Niveau | Écriture | Signification |
|---|---|---|
| Public | nom | accessible partout — le comportement par défaut |
| Protégé | _nom | usage interne recommandé — « n'y touchez pas sans raison » |
| Privé | __nom | accès fortement restreint |
Le cours le dit avec justesse : c'est « davantage une règle de bonnes pratiques qu'une
véritable protection ». Concrètement, un attribut __age est simplement
renommé par Python en _Personne__age (on parle de
name mangling) :
p1.__age # ✘ AttributeError — l'accès direct échoue…
p1._Personne__age # ✔ …mais celui-ci fonctionne !L'encapsulation en Python signale une intention plutôt qu'elle n'interdit. Elle protège des erreurs d'inattention, pas d'un développeur déterminé. C'est suffisant : l'objectif est la cohérence du code, pas la sécurité.
07Getters, setters et @property
Un attribut privé ne se modifie plus directement : on passe par des méthodes d'accès.
| Getter | permet de lire la valeur |
| Setter | permet de la modifier, en effectuant des contrôles |
class Personne:
def __init__(self, age):
self.__age = age
def get_age(self):
return self.__age
def set_age(self, age):
if age >= 0:
self.__age = age
else:
print("Âge invalide")
p1 = Personne(30)
p1.set_age(35)
print(p1.get_age()) # 35
p1.set_age(-5) # Âge invalide
print(p1.get_age()) # 35 ← la dernière valeur VALIDE est conservéeLe setter empêche l'enregistrement de données incohérentes : l'objet ne peut plus se retrouver dans un état absurde.
@property — la forme moderne
Python permet de garder la simplicité d'un attribut public tout en gardant le contrôle d'un setter.
class Personne:
def __init__(self, age):
self.__age = age
@property
def age(self): # le getter
return self.__age
@age.setter
def age(self, valeur): # le setter
if valeur >= 0:
self.__age = valeur
else:
print("Âge invalide")
p1 = Personne(30)
p1.age = 35 # appelle le SETTER sans qu'on le voie
print(p1.age) # 35 ← appelle le GETTER
p1.age = -5 # Âge invalide
print(p1.age) # 35@property est préférableLa syntaxe d'utilisation redevient naturelle (p1.age = 35 au lieu de
p1.set_age(35)) alors que le contrôle est bien là.
Surtout, cela permet de protéger après coup un attribut qui était public,
sans modifier le code qui l'utilise déjà. Avec des get_/set_,
il faudrait réécrire tous les appels existants.
08L'héritage
L'héritage permet à une classe (la fille) de reprendre les attributs et méthodes d'une autre (la mère), puis d'ajouter ce qui lui est propre. Objectif : ne pas réécrire ce qui est commun.
class Equipement: # classe MÈRE
def __init__(self, ip):
self.__ip = ip
def get_ip(self):
return self.__ip
class Serveur(Equipement): # classe FILLE
def __init__(self, ip, systeme):
super().__init__(ip) # on réutilise le constructeur de la mère
self.systeme = systeme
def afficher_infos(self):
print(f"Serveur {self.get_ip()} — {self.systeme}")
s = Serveur("192.168.1.10", "Linux")
s.afficher_infos() # Serveur 192.168.1.10 — Linux
print(s.get_ip()) # 192.168.1.10 ← méthode HÉRITÉELa syntaxe tient en une paire de parenthèses : class Fille(Mere):.
09Override et super()
Redéfinir une méthode (override)
Une classe fille peut remplacer une méthode héritée pour adapter le comportement.
class Utilisateur:
def se_connecter(self):
print("Connexion standard utilisateur")
class Administrateur(Utilisateur):
def se_connecter(self): # même nom → REDÉFINITION
print("Connexion administrateur avec privilèges")
u = Utilisateur()
a = Administrateur()
u.se_connecter() # Connexion standard utilisateur
a.se_connecter() # Connexion administrateur avec privilègesL'override permet de personnaliser un comportement selon le type d'objet, tout en gardant une structure commune.
super() — appeler la mère
super() appelle une méthode de la classe parent depuis la classe enfant, pour
réutiliser son code au lieu de le recopier.
class Administrateur(Utilisateur):
def se_connecter(self):
super().se_connecter() # on fait d'abord ce que fait la mère
print("Privilèges administrateur activés") # puis on ajoute
# Connexion standard utilisateur
# Privilèges administrateur activéssuper().__init__()class Serveur(Equipement):
def __init__(self, ip, systeme):
self.systeme = systeme # ✘ on a oublié super().__init__(ip)
s = Serveur("192.168.1.10", "Linux")
s.get_ip()
# AttributeError: 'Serveur' object has no attribute '_Equipement__ip'Définir __init__ dans la fille remplace celui de la mère : si
on ne l'appelle pas, les attributs de la mère ne sont jamais créés. L'objet semble
exister, puis plante à la première utilisation d'une méthode héritée.
__init__,sa première ligne est presque toujours
super().__init__(…)super() vous épargneSans lui, on recopie le code du parent dans l'enfant. Le jour où le parent change — un
contrôle ajouté, un attribut de plus — les copies ne suivent pas. Avec
super(), les modifications du parent sont automatiquement prises en compte
partout.
Ce code affiche AttributeError à la dernière ligne. Trouvez les
deux problèmes.
class Equipement:
def __init__(self, ip):
self.__ip = ip
class Routeur(Equipement):
def __init__(self, ip, interfaces):
self.interfaces = interfaces
r = Routeur("10.0.0.1", 4)
print(r.__ip)1. super().__init__(ip) est manquant. Le constructeur de
Routeur remplace celui d'Equipement, qui n'est donc jamais
exécuté : l'attribut __ip n'existe pas.
2. r.__ip ne fonctionnerait de toute façon pas. L'attribut est
privé : Python l'a renommé en _Equipement__ip. On ne le lit pas de
l'extérieur — c'est précisément le but.
class Equipement:
def __init__(self, ip):
self.__ip = ip
def get_ip(self): # un accesseur, puisque c'est privé
return self.__ip
class Routeur(Equipement):
def __init__(self, ip, interfaces):
super().__init__(ip) # on construit la partie « mère »
self.interfaces = interfaces
r = Routeur("10.0.0.1", 4)
print(r.get_ip()) # 10.0.0.1Ces deux erreurs sont les plus fréquentes quand on découvre les classes — et elles se produisent souvent ensemble.