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.
💡 La voiture — l'exemple du cours

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éristiquesdeviennent des attributs
les actionsdeviennent des méthodes
la voituredevient 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 Voiture le plan (une seule fois) v1 "Renault", rouge vitesse 0 v2 "Peugeot", bleu vitesse 90 v3 "Tesla", blanc vitesse 130
Une classe, autant d'objets qu'on veut. Chacun a ses propres valeurs.
python — la structure standard
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
        pass
python — créer des objets
class 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 valeurs
🧰 Le nom des classes prend une majuscule

Convention 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.

python
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.

python
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 jamais
python
class 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 given

Python 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 :

python
def afficher():                # ✘ self manquant
    print(self.ip)
# TypeError: afficher() takes 0 positional arguments but 1 was given

Le 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 ?

python
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                          ← commun
🧰 Quand utiliser l'un ou l'autre

Instance : 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.

python
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/h
💡 L'objet sait faire, on ne le bricole pas de l'extérieur

Sans 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.

python
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
🔧 Corrigé — le destructeur s'écrit __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 :

python — le problème
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)           # -10

L'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ÉcritureSignification
Publicnomaccessible partout — le comportement par défaut
Protégé_nomusage interne recommandé — « n'y touchez pas sans raison »
Privé__nomaccès fortement restreint
⚠️ Le « privé » de Python n'est pas une serrure

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) :

python
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.

Getterpermet de lire la valeur
Setterpermet de la modifier, en effectuant des contrôles
python
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ée

Le 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.

python
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
🧰 Pourquoi @property est préférable

La 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.

Equipement ip · get_ip() · set_ip() Serveur + systeme_exploitation Routeur + nb_interfaces héritent de l'IP et de ses accesseurs
Tout équipement a une IP ; seuls certains ont un système d'exploitation. On écrit l'IP une seule fois.
python
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ÉE

La 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.

python
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èges

L'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.

python
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és
⚠️ Oublier super().__init__()
python
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.

Dès qu'une classe fille a son propre __init__,
sa première ligne est presque toujours super().__init__(…)
🧰 Ce que super() vous épargne

Sans 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.

🧠 Auto-évaluation

Ce code affiche AttributeError à la dernière ligne. Trouvez les deux problèmes.

python
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.

python — corrigé
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.1

Ces deux erreurs sont les plus fréquentes quand on découvre les classes — et elles se produisent souvent ensemble.

⚡ Récap 2 minutes

  • Classe = le plan · objet = une réalisation. class Personne:, majuscule.
  • __init__ construit, self désigne l'objet. On déclare self, on ne le passe jamais.
  • Attribut de classe = partagé · d'instance = propre (dans __init__).
  • Destructeur : __del__ — et non __def__.
  • Encapsulation : nom public, _nom protégé, __nom privé. Une convention, pas une serrure.
  • Getter / setter pour contrôler, ou mieux : @property.
  • Héritage : class Fille(Mere): — on ne réécrit pas le commun.
  • Override : même nom de méthode dans la fille. super() appelle la mère.
  • Ne jamais oublier super().__init__(…) dans le constructeur d'une fille.

Sources exploitées

  • Cours « POO en Python » · « L'Encapsulation » · « L'héritage en Python » (le professeur, BTS CIEL 2025-2026)
  • TP3 et TP4 — POO, encapsulation et héritage (gestion d'équipements réseau)
  • comptes rendus d'élève