Skip to content

Paradigme orienté objets

Définition

En programmation orientée objet, un programme est un ensemble d'entités qui interagissent. Ces entités sont appelées des objets. Un objet possède un état (les données qui le caractérisent) et des comportements (ce qu'il sait faire).

L'état d'un objet est représenté par l'ensemble de ses attributs Les comportements d'un objet sont représentés par l'ensemble de ses méthodes

Une classe permet de définir une famille d'objets. A partir d'une classe, on peut créer autant d'objets que l'on veut. Ce sont des exemplaires, des instances de la classe.

Le facile et le difficile

Écrire une classe (ses attributs, ses méthodes) est la partie visible et statique. Le vrai enjeu, plus difficile, est la dynamique : ce qui se passe à l'exécution, quand on crée des objets, qu'on les relie par des références, et qu'ils s'envoient des messages (les appels de méthodes). C'est cette dynamique qu'on va tracer, pas seulement la syntaxe.

Classe Animal

Voici un exemple basique d'une classe en Python qui modélise la phrase "Un animal porte un nom, et il sait parler."

class Animal:
    def __init__(self, nom: str) -> None:
        self.nom = nom  # Attribut d'instance

    def parler(self) -> None:   # Méthode d'instance
        print(f"{self.nom} fait du bruit.")

x = Animal("Bidule")   # création d'une instance d'Animal portant le nom bidule
print(x.nom)           # On accède aux attributs de l'instance avec le point. Affiche: Bidule
x.nom = "Truc"         # Mutabilité -> On peut modifier les attributs de l'instance de la même manière 
x.parler()  # On accède aussi aux méthodes d'instance par le point. Affiche: Truc fait du bruit.

Prédis la sortie

Avant d'exécuter, écris ce que ce programme affiche, puis déplie pour vérifier.

Réponse
Bidule
Truc fait du bruit.

Dans cet exemple :

  • __init__ : Cette méthode sera automatiquement chargée d'initialiser les attributs de l'objet en cours d'instanciation. On le considère comme le constructeur de la classe.
  • self : Représente l'instance actuelle de la classe. Il permet d'accéder aux attributs et méthodes de l'objet lui-même.

Constructeur

Un constructeur est une fonction particulière appelée lors de l'instanciation. Elle permet d'allouer la mémoire nécessaire à l'objet et d'initialiser ses attributs.

Abus de langage

Il est très (trop) courant de considérer __init__ comme le constructeur en Python, car il initialise l'objet après sa création. Cependant, il ne fait que la moitié du travail. la méthode chargée de créer l'instance est le dunder __new__. La preuve en est que __init__ prend self en paramètre, c'est donc bien que self existe déjà avant l'invocation d'__init__. Le constructeur est en réalité le couple formé par les méthodes __new__ et __init__.

Voici une portion de code pour se rendre compte de ce qu'il se passe réellement:

class MaClasse:
    def __new__(cls, *args, **kwargs) -> "MaClasse":
        '''Appel de __new__ pour créer une nouvelle instance de la classe'''
        print("Appel de __new__ : Création de l'instance")
        instance = super(MaClasse, cls).__new__(cls)
        return instance

    def __init__(self, valeur) -> None:
        '''Appel de __init__ pour initialiser l'instance avec les attributs'''
        print("Appel de __init__ : Initialisation de l'instance")
        self.valeur = valeur

    def afficher_valeur(self) -> None:
        print(f"Valeur : {self.valeur}")

# Instanciation d'un objet
c = MaClasse("truc")
c.afficher_valeur()

On considèrera malgré tout que __init__ est le constructeur, car si on te pose la question, c'est la réponse attendue.

Une variable désigne un objet : les références

Le point le plus délicat de l'objet n'est pas la syntaxe des classes, c'est de suivre ce qui se passe à l'exécution. En particulier : que contient une variable comme x après x = Animal("Bidule") ?

x ne contient pas l'animal. x désigne l'objet : elle en garde une référence (une flèche vers lui, quelque part en mémoire). C'est le même mécanisme que le partage de référence rencontré avec les listes.

Conséquence directe : deux variables peuvent désigner le même objet.

a = Animal("Rocket")
b = a               # b et a désignent le MÊME objet
b.nom = "Groot"
print(a.nom)        # Groot : modifier via b a changé a

Piège : une référence n'est pas une copie

b = a ne crée pas un second animal : a et b sont deux noms pour le même objet. Modifier l'un modifie l'autre. Pour savoir si deux variables désignent le même objet (et non deux objets seulement égaux), on utilise is :

c = Animal("Rocket")
d = Animal("Rocket")
print(c is d)          # False : deux objets distincts...
print(c.nom == d.nom)  # ...mais qui portent le même nom

« Qui est l'objet ? » est une vraie question : un même objet peut apparaître sous plusieurs noms.

self, c'est l'objet courant

Dans une méthode, self désigne l'instance sur laquelle la méthode est appelée. Quand on écrit a.parler(), la méthode parler s'exécute avec self valant a. Chaque objet a ses propres attributs : self.nom est le nom de cet objet-là, pas d'un autre.

Trace la dynamique

Copie ces exemples dans Python Tutor et avance pas à pas : tu verras les objets créés en mémoire et les flèches (les références) qui pointent vers eux. C'est la meilleure façon de voir ce que la simple lecture du code ne montre pas.

Interactions entre objets

Rajoutons la phrase "Un Humain porte un nom, un prenom et peut adopter des animaux domestiques. On peut afficher la liste des animaux d'un Humain".

class Humain:
    def __init__(self, prenom: str, nom: str) -> None:
        self.nom = nom
        self.prenom = prenom
        self.animaux: list[Animal] = []

    def adopte(self, a: Animal) -> None:
        self.animaux.append(a)

    def afficher_animaux(self) -> None:
        for a in self.animaux:
            print(a.nom)


x = Humain("Peter", "Quill")
y = Animal("Rocket")
x.adopte(y)
x.afficher_animaux()   # Affiche Rocket

Ici, on a choisi que l'humain porte la liste de ses animaux. On aurait pu à la place ajouter un attribut propriétaire à la classe Animal pour dire qu'un animal est la propriété d'un humain particulier. Mais on aurait perdu la possibilité d'afficher les animaux d'un humain sans disposer de la liste de tous les animaux. Il faut souvent choisir quelle classe est le "chef d'orchestre" en fonction du problème qu'on a à traiter.

Modélisation objet

Une voiture a une marque, un modèle, un prix, un nombre de kilomètres parcourus, une quantite de carburant dans le réservoir (en litres), et une consommation (litres au 100). Une voiture peut rouler sur une certaine distance. À chaque fois qu'elle roule, le kilométrage augmente, et le carburant diminue. Le réservoir de la voiture a une capacité maximale. On peut afficher les caractéristiques de la voiture.

Un Humain a une quantité d'argent disponible sur son compte en banque. Un Humain peut acheter des voitures. Un Humain peut faire des trajets avec une voiture pour un certain nombre de kilomètres. Avant de commencer un trajet, l'humain vérifie s'il a assez d'essence, sinon, il va à la station service faire le plein. Lorsqu'un humain fait le plein de sa voiture, son compte en banque diminue.

  • Écris une classe Voiture et une classe Humain compatibles avec cette description, puis instancie-les pour jouer un scénario.

Méthode (une nouveauté à la fois).

  1. Spécifie avant de coder. Pour chaque classe, écris d'abord les signatures typées de ses méthodes et 2-3 scénarios de test attendus (par exemple : après v.roule(100), de combien baisse le carburant ?). C'est ce qui te permettra de dire si le code obtenu, le tien ou un autre, fait bien ce qui était prévu.
  2. Écris et teste Voiture d'abord (rouler, faire le plein, afficher), puis Humain (acheter une voiture, faire un trajet en vérifiant le carburant).
  3. Instancie un Humain et une Voiture, et joue un scénario complet (achat, trajet, passage à la station).
Indice : squelette de Voiture
class Voiture:
    def __init__(self, marque: str, modele: str, prix: float,
                 conso: float, capacite: float) -> None:
        self.marque = marque
        self.modele = modele
        self.prix = prix
        self.conso = conso            # litres au 100 km
        self.capacite = capacite      # capacité du réservoir
        self.km = 0.0
        self.carburant = 0.0

    def roule(self, distance: float) -> None:
        ...   # augmente self.km, diminue self.carburant selon la conso

    def plein(self) -> float:
        ...   # remplit le réservoir, renvoie le nombre de litres ajoutés

    def affiche(self) -> None:
        ...
Ce que l'IA ne change pas

Une IA écrit ces deux classes en une seconde, et elle proposera aussi une répartition des attributs et des tests. Elle n'est incapable de rien de tout cela.

Mais décider quels attributs portent l'état (le carburant dans Voiture, l'argent dans Humain) et quelle classe est le chef d'orchestre, ce sont des choix de modélisation : il y a plusieurs réponses défendables, et celle qui te va dépend de ce que ton programme doit faire. Personne ne peut faire ce choix à ta place sans savoir où tu vas, et tu dois pouvoir le justifier. Signatures typées et tests d'abord, corps ensuite.