Maîtrise de la POO en Python : Concepts avancés pour l'IA

Introduction à la programmation orientée objet en Python

Python est un langage qui implémente nativement les paradigmes de la programmation orientée objet (POO). La maîtrise de ces concepts est indispensable pour concevoir des applications modulaires, maintenables et évolutives, notamment dans le domaine de l'intelligence artificielle.

  1. Notions fondamentales de l'orientation objet

Concept Description
Classe Modèle définissant la structure (attributs) et le comportement (méthodes) d'un objet.
Objet Instance concrète d'une classe, possédant un état propre.
Encapuslation Regroupement des données et des opérations, avec masquage de l'implémentation interne.
Héritage Mécanisme permettant à une classe (fille) de réutiliser les propriétés d'une autre classe (mère).
Polymorphisme Capacité d'utiliser une interface unique pour des types d'objets différents.
Abstraction Simplification d'un système en ne montrant que les fonctionnalités essentielles.
  1. Création de classes et instanciation d'objets

Syntaxe de base

class NomDeLaClasse:
    # Attribut de classe (partagé par toutes les instances)
    attribut_partage = "valeur partagée"

    # Constructeur : initialise l'état de l'objet
    def __init__(self, param1, param2):
        self.attribut_instance1 = param1
        self.attribut_instance2 = param2

    # Méthode d'instance
    def afficher(self):
        return f"{self.attribut_instance1} - {self.attribut_instance2}"

# Instanciation
objet = NomDeLaClasse("valeur1", "valeur2")
print(objet.afficher())  # résultat : valeur1 - valeur2

Le rôle de __init__

Cette méthode spéciale est automatiquement appelée lors de la création d'un objet. Le paramètre self fait référence à l'instance en cours de création.

class Utilisateur:
    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    def saluer(self):
        return f"Bonjour, je m'appelle {self.nom} et j'ai {self.age} ans."

u = Utilisateur("Bob", 30)
print(u.saluer())  # Bonjour, je m'appelle Bob et j'ai 30 ans.
  1. Encapsulation et contrôle d'accès

Python ne possède pas de modificateurs d'accès stricts comme en Java, mais utilise des conventions de nommmage pour indiquer le niveau de visibilité.

Convention Visibilité
nom Public : accessible partout.
_nom Protégé : usage interne recommandé.
__nom Privé : subit un "name mangling" (renommage) pour limiter l'accès directt.

Exemple pratique

class CompteBancaire:
    def __init__(self, proprietaire, solde=0):
        self.proprietaire = proprietaire    # public
        self._solde = solde                 # protégé
        self.__code_pin = "5678"            # privé

    def deposer(self, montant):
        if montant > 0:
            self._solde += montant

    def get_solde(self, code):
        if code == self.__code_pin:
            return self._solde
        else:
            raise ValueError("Code PIN incorrect")

cb = CompteBancaire("Alice", 500)
print(cb.proprietaire)        # ✅ accessible
print(cb._solde)              # ⚠️ techniquement accessible, déconseillé
# print(cb.__code_pin)        # ❌ erreur
print(cb._CompteBancaire__code_pin)  # ✅ mais ne doit pas être utilisé

Bonne pratique : Utiliser les propriétés (@property) pour un accès contrôlé.

class Celsius:
    def __init__(self, temperature=0):
        self._temperature = temperature

    @property
    def temperature(self):
        return self._temperature

    @temperature.setter
    def temperature(self, valeur):
        if valeur < -273.15:
            raise ValueError("Température inférieure au zéro absolu impossible")
        self._temperature = valeur

c = Celsius(25)
print(c.temperature)  # 25
c.temperature = -300  # déclenche une erreur
  1. Héritage

L'héritage permet de créer une classe dérivée qui hérite des attributs et méthodes de la classe parente.

Héritage simple

class Animal:
    def __init__(self, nom):
        self.nom = nom

    def parler(self):
        raise NotImplementedError("Les sous-classes doivent implémenter parler()")

class Chien(Animal):
    def parler(self):
        return f"{self.nom} aboie !"

class Chat(Animal):
    def parler(self):
        return f"{self.nom} miaule !"

medor = Chien("Médor")
minou = Chat("Minou")
print(medor.parler())  # Médor aboie !
print(minou.parler())  # Minou miaule !

Appel au parent avec super()

class Employe:
    def __init__(self, nom, salaire):
        self.nom = nom
        self.salaire = salaire

class Manager(Employe):
    def __init__(self, nom, salaire, prime):
        super().__init__(nom, salaire)  # appel au constructeur parent
        self.prime = prime

    def revenu_total(self):
        return self.salaire + self.prime

Héritage multiple

class Volant:
    def voler(self):
        return "Je vole..."

class Nageur:
    nager = lambda self: "Je nage..."

class Canard(Animal, Volant, Nageur):
    def parler(self):
        return "Coin coin !"

donald = Canard("Donald")
print(donald.voler())   # Je vole...

Attention : L'ordre de résolution des méthodes (MRO) peut être complexe. Utilisez Classe.__mro__ pour le vérifier.

  1. Polymorphisme

Grâce au duck typing de Python, le polymorphisme est naturel.

def faire_parler(animal: Animal):
    print(animal.parler())

animaux = [Chien("Rex"), Chat("Félix"), Canard("Daisy")]
for a in animaux:
    faire_parler(a)
# Rex aboie !
# Félix miaule !
# Daisy coin coin !
  1. Méthodes spéciales (dunder methods)

Ces méthodes, encadrées par des doubles underscores, permettent de personnaliser le comportement des objets.

Méthode Rôle
__str__ Représentation lisible pour l'utilisateur (ex: print(obj))
__repr__ Représentation technique pour le développeur
__len__ Support de la fonction len()
__getitem__ Accès par index (ex: objet[0])
__call__ Rend l'objet "appelable" comme une fonction
__eq__ Définit l'égalité entre deux objets

Exemple : conteneur personnalisé

class ListePerso:
    def __init__(self, elements=None):
        self._elements = elements if elements else []

    def __len__(self):
        return len(self._elements)

    def __getitem__(self, index):
        return self._elements[index]

    def __str__(self):
        return f"ListePerso({self._elements})"

ma_liste = ListePerso([10, 20, 30])
print(len(ma_liste))  # 3
print(ma_liste[1])    # 20
  1. Méthodes de classe et méthodes statiques

Type Décorateur Premier paramètre Usage
Méthode d'instance Aucun self Opère sur l'instance
Méthode de classe @classmethod cls Opère sur la classe (ex: constructeur alternatif)
Méthode statique @staticmethod Aucun Fonction utilitaire liée logiquement à la classe
class Personne:
    espece = "Homo sapiens"

    def __init__(self, nom, age):
        self.nom = nom
        self.age = age

    @classmethod
    def depuis_chaine(cls, chaine):
        nom, age = chaine.split('-')
        return cls(nom, int(age))

    @staticmethod
    def est_majeur(age):
        return age >= 18

p = Personne.depuis_chaine("Alice-28")
print(Personne.est_majeur(16))  # False
  1. Classes abstraites (ABC)

Elles imposent un contrat aux sous-classes, qui doivent implémenter les méthodes abstraites.

from abc import ABC, abstractmethod

class Forme(ABC):
    @abstractmethod
    def aire(self):
        pass

    @abstractmethod
    def perimetre(self):
        pass

class Rectangle(Forme):
    def __init__(self, largeur, hauteur):
        self.largeur = largeur
        self.hauteur = hauteur

    def aire(self):
        return self.largeur * self.hauteur

    def perimetre(self):
        return 2 * (self.largeur + self.hauteur)

# f = Forme()  # erreur
r = Rectangle(3, 4)
print(r.aire())  # 12
  1. Data classes (@dataclass)

Depuis Python 3.7, ce décorateur simplifie la création de classes principalement destinées à stocker des données.

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

    def distance_origine(self):
        return (self.x**2 + self.y**2) ** 0.5

pt = Point(3.0, 4.0)
print(pt)  # Point(x=3.0, y=4.0)
print(pt.distance_origine())  # 5.0

Les méthodes __init__, __repr__ et __eq__ sont générées automatiquement.

  1. Principes SOLID (synthèse)

  1. Responsabilité unique : Une classe ne doit avoir qu'une seule raison de changer.

  2. Ouvert/fermé : Les classes doivent être ouvertes à l'extension, fermées à la modification.

  3. Substitution de Liskov : Une sous-classe doit pouvoir remplacer sa classe mère sans altérer le programme.

  4. Ségrégation des interfaces : Préférer des interfaces spécifiques plutôt qu'une interface générale.

  5. Inversion des dépendances : Dépendre d'abstractions, pas d'implémentations concrètes.

  6. Utilisation avancée de super()


super() ne se limite pas à appeler le constructeur parent. Il gère correctement l'héritage multiple via le MRO.

  1. Conclusion

La POO en Python offre une flexibilité remarquable pour structurer des applications complexes, notamment en IA. Voici un récapitulatif des mécanismes clés :

Concept Implémentation Python
Encapsulation Conventions de nommage + @property
Héritage class Fille(Parent): + super()
Polymorphisme Duck typing + redéfinition de méthodes
Abstraction abc.ABC + @abstractmethod
Classes de données @dataclass

Recommandations : Favoriser la composition sur l'héritage, utiliser les propriétés pour un contrôle fin, et recourir aux data classes pour les modèles simples.

Étiquettes: Python Programmation orientée objet Héritage polymorphisme encapsulation

Publié le 21 juillet à 00h39