Cours Python · Module 3: Programmation orientée objet

Méthodes magiques - comportements cachés des animaux

14 min de lecture
Dans cette leçon11

Bon retour ! Darwin ici, avec une leçon fascinante sur les pouvoirs cachés de Python.

Dans la nature, les animaux ont des comportements cachés - un lion rugit, un éléphant barrit, un caméléon se camoufle. En Python, les classes peuvent aussi avoir des comportements cachés grâce aux méthodes magiques - des méthodes spéciales entourées de doubles underscores !

1lion = Animal("Lion")
2print(lion)  # Comment Python sait-il quoi afficher ? __str__ !
3len(lion)    # Comment Python sait-il quoi mesurer ? __len__ !
4lion + elephant  # Comment Python sait-il quoi additionner ? __add__ !

Que sont les méthodes magiques ?

Les méthodes magiques (aussi appelées méthodes dunder, de « double underscore ») sont des méthodes spéciales au format __method__ que Python appelle automatiquement dans des situations précises.

Analogie Safari : elles sont comme les instincts naturels des animaux - tu n'as pas à les leur apprendre, ils sont inscrits dans leur nature !

1class Animal:
2    def __init__(self, name):
3        """Appelée à la CRÉATION d'un objet"""
4        self.name = name
5
6    def __str__(self):
7        """Appelée par print() et str()"""
8        return f"Animal : {self.name}"
9
10    def __len__(self):
11        """Appelée par len()"""
12        return len(self.name)
13
14lion = Animal("Lion")  # Appelle __init__
15print(lion)           # Appelle __str__
16print(len(lion))      # Appelle __len__

Pourquoi « magiques » ?

  • Python les appelle automatiquement
  • Elles font que tes classes se comportent comme les types intégrés
  • Elles permettent d'utiliser la syntaxe Python standard

Méthodes magiques de base

1. __init__ - Constructeur

Tu connais déjà celle-ci ! Appelée à la création d'un objet.

1class Species:
2    def __init__(self, name, population):
3        """Initialisation - tu connais déjà !"""
4        self.name = name
5        self.population = population
6
7lion = Species("Lion", 500)  # Appelle __init__

2. __str__ - Représentation lisible par l'humain

Appelée par print() et str() - renvoie une chaîne conviviale. Quand tu écris print(lion), print() convertit d'abord l'argument en texte avec str(lion), et str() appelle la méthode lion.__str__().

1class Animal:
2    def __init__(self, name, age):
3        self.name = name
4        self.age = age
5
6    def __str__(self):
7        """Pour les humains - lisible"""
8        return f"{self.name} (âge : {self.age} ans)"
9
10lion = Animal("Simba", 5)
11print(lion)  # "Simba (âge : 5 ans)" - appelle __str__
12print(str(lion))  # La même chose

3. __repr__ - Représentation pour le programmeur

Appelée par repr() et dans la console - renvoie une représentation non ambiguë de l'objet.

1class Animal:
2    def __init__(self, name, age):
3        self.name = name
4        self.age = age
5
6    def __repr__(self):
7        """Pour les programmeurs - précise"""
8        return f"Animal(name='{self.name}', age={self.age})"
9
10    def __str__(self):
11        """Pour les utilisateurs - lisible"""
12        return f"{self.name} ({self.age} ans)"
13
14lion = Animal("Simba", 5)
15
16# Dans la console Python
17>>> lion
18Animal(name='Simba', age=5)  # Appelle __repr__
19
20# Dans print
21print(lion)  # "Simba (5 ans)" - appelle __str__
22
23# repr() explicitement
24print(repr(lion))  # "Animal(name='Simba', age=5)"

Règle : __repr__ devrait renvoyer du code capable de recréer l'objet !

1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6    def __repr__(self):
7        return f"Point({self.x}, {self.y})"
8
9p = Point(3, 4)
10print(repr(p))  # "Point(3, 4)" - tu peux le copier-coller !

Méthodes de comparaison

Permettent de comparer des objets avec ==, <, >, etc.

1class Animal:
2    def __init__(self, name, weight):
3        self.name = name
4        self.weight = weight  # kg
5
6    def __eq__(self, other):
7        """Égalité : =="""
8        if not isinstance(other, Animal):
9            return False
10        return self.weight == other.weight
11
12    def __lt__(self, other):
13        """Inférieur : <"""
14        if not isinstance(other, Animal):
15            return NotImplemented
16        return self.weight < other.weight
17
18    def __le__(self, other):
19        """Inférieur ou égal : <="""
20        return self.weight <= other.weight
21
22    def __gt__(self, other):
23        """Supérieur : >"""
24        return self.weight > other.weight
25
26    def __ge__(self, other):
27        """Supérieur ou égal : >="""
28        return self.weight >= other.weight
29
30    def __ne__(self, other):
31        """Différent : !="""
32        return not self.__eq__(other)
33
34lion = Animal("Lion", 190)
35elephant = Animal("Éléphant", 5000)
36rhino = Animal("Rhinocéros", 2300)
37
38print(lion < elephant)  # True - appelle __lt__
39print(elephant > rhino)  # True - appelle __gt__
40print(lion == Animal("Tigre", 190))  # True - même poids

Astuce : tu peux utiliser le décorateur @functools.total_ordering - définis seulement __eq__ et l'une de __lt__, __le__, __gt__, __ge__, et le reste sera généré automatiquement !

1from functools import total_ordering
2
3@total_ordering
4class Animal:
5    def __init__(self, name, weight):
6        self.name = name
7        self.weight = weight
8
9    def __eq__(self, other):
10        """Seulement celle-ci et __lt__ - le reste automatiquement !"""
11        if not isinstance(other, Animal):
12            return False
13        return self.weight == other.weight
14
15    def __lt__(self, other):
16        """Seulement celle-ci et __eq__ - le reste automatiquement !"""
17        if not isinstance(other, Animal):
18            return NotImplemented
19        return self.weight < other.weight
20
21    # __le__, __gt__, __ge__, __ne__ - générées automatiquement !

Méthodes arithmétiques

Permettent d'utiliser les opérateurs mathématiques avec les objets !

1class Population:
2    """Représente la population d'une espèce"""
3
4    def __init__(self, species, count):
5        self.species = species
6        self.count = count
7
8    def __add__(self, other):
9        """Addition : +"""
10        if isinstance(other, Population):
11            if self.species != other.species:
12                raise ValueError("Espèces différentes !")
13            return Population(self.species, self.count + other.count)
14        elif isinstance(other, int):
15            return Population(self.species, self.count + other)
16        return NotImplemented
17
18    def __sub__(self, other):
19        """Soustraction : -"""
20        if isinstance(other, int):
21            return Population(self.species, max(0, self.count - other))
22        return NotImplemented
23
24    def __mul__(self, other):
25        """Multiplication : *"""
26        if isinstance(other, (int, float)):
27            return Population(self.species, int(self.count * other))
28        return NotImplemented
29
30    def __str__(self):
31        return f"{self.species} : {self.count} individus"
32
33# Utilisation des opérateurs !
34lions_north = Population("Lion", 120)
35lions_south = Population("Lion", 85)
36
37# Addition de populations
38total_lions = lions_north + lions_south
39print(total_lions)  # "Lion : 205 individus"
40
41# Addition d'un nombre
42more_lions = lions_north + 30
43print(more_lions)  # "Lion : 150 individus"
44
45# Soustraction (baisse de population)
46fewer_lions = lions_north - 20
47print(fewer_lions)  # "Lion : 100 individus"
48
49# Multiplication (estimation de la croissance)
50projected = lions_north * 1.5  # Croissance de 50 %
51print(projected)  # "Lion : 180 individus"

Méthodes de conteneur

Font en sorte que tes objets se comportent comme des listes, des dictionnaires !

__len__ - Longueur

Quand une classe a une méthode __len__, la fonction len() fonctionne sur ses objets comme sur une liste. La meute renvoie le nombre de ses membres :

1class Pack:
2    """Meute d'animaux"""
3
4    def __init__(self, species):
5        self.species = species
6        self.members = []
7
8    def add(self, name):
9        self.members.append(name)
10
11    def __len__(self):
12        """Appelée par len()"""
13        return len(self.members)
14
15pack = Pack("Lions")
16pack.add("Simba")
17pack.add("Nala")
18pack.add("Mufasa")
19
20print(len(pack))  # 3 - appelle __len__

__getitem__ et __setitem__ - Accès par index

Ces deux méthodes permettent de lire et de modifier les éléments d'un objet avec des crochets, comme dans une liste. Au passage, l'habitat reçoit aussi __contains__, qui gère l'opérateur in :

1class Habitat:
2    """Habitat avec des animaux"""
3
4    def __init__(self, name):
5        self.name = name
6        self.animals = []
7
8    def __getitem__(self, index):
9        """Accès en lecture : habitat[0]"""
10        return self.animals[index]
11
12    def __setitem__(self, index, value):
13        """Accès en écriture : habitat[0] = "Lion" """
14        self.animals[index] = value
15
16    def __len__(self):
17        return len(self.animals)
18
19    def __contains__(self, item):
20        """Opérateur 'in' : "Lion" in habitat"""
21        return item in self.animals
22
23    def append(self, animal):
24        self.animals.append(animal)
25
26savanna = Habitat("Savane")
27savanna.append("Lion")
28savanna.append("Éléphant")
29savanna.append("Girafe")
30
31# Accès comme une liste !
32print(savanna[0])  # "Lion" - appelle __getitem__
33savanna[1] = "Rhinocéros"  # Appelle __setitem__
34
35# Opérateur in
36print("Lion" in savanna)  # True - appelle __contains__
37
38# len()
39print(len(savanna))  # 3 - appelle __len__
40
41# Itération (automatique via __getitem__)
42for animal in savanna:
43    print(animal)

__iter__ et __next__ - Itération

Une boucle for appelle d'abord __iter__, puis __next__ à chaque tour, jusqu'à recevoir l'exception StopIteration. C'est ainsi que nous allons construire un calendrier des jours d'expédition :

1class Expedition:
2    """Itérateur sur les jours d'expédition"""
3
4    def __init__(self, days):
5        self.days = days
6        self.current_day = 0
7
8    def __iter__(self):
9        """Renvoie l'itérateur (self)"""
10        self.current_day = 0
11        return self
12
13    def __next__(self):
14        """Renvoie l'élément suivant"""
15        if self.current_day >= self.days:
16            raise StopIteration  # Fin de l'itération
17
18        self.current_day += 1
19        return f"Jour {self.current_day}"
20
21expedition = Expedition(5)
22
23# Itération avec for
24for day in expedition:
25    print(day)
26# Jour 1
27# Jour 2
28# Jour 3
29# Jour 4
30# Jour 5
31
32# Ou manuellement
33exp2 = Expedition(3)
34print(next(exp2))  # "Jour 1"
35print(next(exp2))  # "Jour 2"
36print(next(exp2))  # "Jour 3"

Un quatrième appel à next(exp2) lèverait l'exception StopIteration. C'est le même signal qui fait s'arrêter une boucle for toute seule.

__call__ - Objets appelables

Rend un objet appelable comme une fonction !

1class AnimalSound:
2    """Appelable - objet comme une fonction"""
3
4    def __init__(self, species, sound):
5        self.species = species
6        self.sound = sound
7
8    def __call__(self, times=1):
9        """Appelle l'objet comme une fonction"""
10        return " ".join([self.sound] * times)
11
12lion_roar = AnimalSound("Lion", "ROAR")
13elephant_trumpet = AnimalSound("Éléphant", "TRUUU")
14
15# Appel comme une fonction !
16print(lion_roar())  # "ROAR" - appelle __call__
17print(lion_roar(3))  # "ROAR ROAR ROAR"
18print(elephant_trumpet(2))  # "TRUUU TRUUU"

Gestionnaires de contexte - __enter__ et __exit__

Permettent d'utiliser l'instruction with !

1class SafariCamera:
2    """Gestionnaire de contexte pour prendre des photos"""
3
4    def __init__(self, location):
5        self.location = location
6        self.photos = []
7
8    def __enter__(self):
9        """Appelée à l'entrée du bloc 'with'"""
10        print(f"Allumage de la caméra à {self.location}")
11        return self  # Renvoie l'objet à utiliser
12
13    def __exit__(self, exc_type, exc_val, exc_tb):
14        """Appelée à la sortie du bloc 'with'"""
15        print(f"Photos sauvegardées : {len(self.photos)}")
16        print(f"Extinction de la caméra")
17        return False  # Ne supprime pas les exceptions
18
19    def take_photo(self, subject):
20        """Prend une photo"""
21        self.photos.append(subject)
22        print(f"  Photo : {subject}")
23
24# Utilisation avec 'with' - __enter__ et __exit__ automatiques !
25with SafariCamera("Serengeti") as camera:
26    camera.take_photo("Lion en chasse")
27    camera.take_photo("Troupeau d'éléphants")
28    camera.take_photo("Girafe près d'un arbre")

À l'exécution, la console affiche :

1Allumage de la caméra à Serengeti
2  Photo : Lion en chasse
3  Photo : Troupeau d'éléphants
4  Photo : Girafe près d'un arbre
5Photos sauvegardées : 3
6Extinction de la caméra

Nous n'avons jamais appelé __enter__ ni __exit__ à la main. with a lancé la première méthode au début du bloc et la seconde automatiquement à la fin, et il le ferait même si une erreur survenait à l'intérieur.

Exemple Safari - système de catalogage complet

Pour finir, une classe d'espèce qui utilise presque toutes les méthodes magiques de cette leçon : ses objets ont une description lisible, se comparent par population, gèrent + et -, se comportent comme un conteneur et s'appellent comme une fonction.

1from functools import total_ordering
2from datetime import datetime
3
4@total_ordering
5class Species:
6    """
7    Classe Species avec un jeu complet de méthodes magiques
8
9    Se comporte comme un type intégré de Python !
10    """
11
12    all_species = []  # Registre de toutes les espèces
13
14    def __init__(self, scientific_name, common_name, population, habitat):
15        """Constructeur"""
16        self.scientific_name = scientific_name
17        self.common_name = common_name
18        self.population = population
19        self.habitat = habitat
20        self.observations = []
21
22        Species.all_species.append(self)
23
24    # === REPRÉSENTATION ===
25
26    def __str__(self):
27        """Pour print() - convivial"""
28        return f"{self.common_name} ({self.population} individus)"
29
30    def __repr__(self):
31        """Pour repr() - non ambigu"""
32        return (f"Species(scientific_name='{self.scientific_name}', "
33                f"common_name='{self.common_name}', "
34                f"population={self.population}, "
35                f"habitat='{self.habitat}')")
36
37    # === COMPARAISONS (seulement __eq__ et __lt__, le reste via @total_ordering) ===
38
39    def __eq__(self, other):
40        """Égalité : =="""
41        if not isinstance(other, Species):
42            return False
43        return self.population == other.population
44
45    def __lt__(self, other):
46        """Inférieur : < (comparaison par population)"""
47        if not isinstance(other, Species):
48            return NotImplemented
49        return self.population < other.population
50
51    def __hash__(self):
52        """Hash du même champ que __eq__ - des objets égaux doivent avoir le même hash"""
53        return hash(self.population)
54
55    # === ARITHMÉTIQUE ===
56
57    def __add__(self, other):
58        """Addition : species + 50"""
59        if isinstance(other, int):
60            return Species(
61                self.scientific_name,
62                self.common_name,
63                self.population + other,
64                self.habitat
65            )
66        return NotImplemented
67
68    def __sub__(self, other):
69        """Soustraction : species - 20"""
70        if isinstance(other, int):
71            return Species(
72                self.scientific_name,
73                self.common_name,
74                max(0, self.population - other),
75                self.habitat
76            )
77        return NotImplemented
78
79    # === CONTENEUR ===
80
81    def __len__(self):
82        """len(species) - nombre d'observations"""
83        return len(self.observations)
84
85    def __getitem__(self, index):
86        """species[0] - accès aux observations"""
87        return self.observations[index]
88
89    def __contains__(self, location):
90        """'Serengeti' in species - vérifie l'emplacement"""
91        return any(obs["location"] == location for obs in self.observations)
92
93    # === APPELABLE ===
94
95    def __call__(self, location, count):
96        """Appel comme une fonction - ajoute une observation"""
97        observation = {
98            "date": datetime.now().strftime("%Y-%m-%d"),
99            "location": location,
100            "count": count
101        }
102        self.observations.append(observation)
103        return f"Ajouté : {count}x {self.common_name} à {location}"
104
105    # === BOOL ===
106
107    def __bool__(self):
108        """bool(species) - l'espèce a-t-elle une population ?"""
109        return self.population > 0
110
111# === DÉMONSTRATION DE TOUTES LES MÉTHODES MAGIQUES ===
112
113print("=== CRÉATION DES ESPÈCES ===\n")
114
115lion = Species("Panthera leo", "Lion", 120, "savane")
116elephant = Species("Loxodonta africana", "Éléphant", 450, "savane")
117rhino = Species("Diceros bicornis", "Rhinocéros", 45, "savane")
118extinct = Species("Dodo", "Dodo", 0, "Maurice")
119
120# __str__ et __repr__
121print("__str__ (print) :")
122print(lion)  # "Lion (120 individus)"
123
124print("\n__repr__ (repr) :")
125print(repr(lion))
126# Species(scientific_name='Panthera leo', common_name='Lion', population=120, habitat='savane')
127
128# Comparaisons (__eq__, __lt__, etc.)
129print("\n=== COMPARAISONS ===")
130print(f"lion == elephant ? {lion == elephant}")  # False
131print(f"rhino < lion ? {rhino < lion}")  # True (45 < 120)
132print(f"elephant > lion ? {elephant > lion}")  # True (450 > 120)
133
134# Tri (fonctionne via __lt__)
135species_list = [lion, rhino, elephant]
136species_list.sort()
137print(f"\nTriées (croissant) : {[str(s) for s in species_list]}")
138# ['Rhinocéros (45 individus)', 'Lion (120 individus)', 'Éléphant (450 individus)']
139
140# Arithmétique (__add__, __sub__)
141print("\n=== ARITHMÉTIQUE ===")
142more_lions = lion + 30  # Augmente la population
143print(f"lion + 30 = {more_lions}")  # "Lion (150 individus)"
144
145fewer_rhinos = rhino - 10
146print(f"rhino - 10 = {fewer_rhinos}")  # "Rhinocéros (35 individus)"
147
148# Appelable (__call__)
149print("\n=== APPELABLE - ajout d'observations ===")
150print(lion("Serengeti", 12))  # Appel comme une fonction !
151print(lion("Masai Mara", 8))
152print(elephant("Amboseli", 35))
153
154# Conteneur (__len__, __getitem__, __contains__)
155print("\n=== CONTENEUR ===")
156print(f"Nombre d'observations du lion : {len(lion)}")  # __len__
157print(f"Première observation : {lion[0]}")  # __getitem__
158print(f"'Serengeti' in lion ? {'Serengeti' in lion}")  # __contains__ - True
159
160# Itération (via __getitem__)
161print("\nToutes les observations du lion :")
162for obs in lion:
163    print(f"  - {obs['date']} à {obs['location']} : {obs['count']} individus")
164
165# Bool (__bool__)
166print("\n=== BOOL ===")
167print(f"bool(lion) ? {bool(lion)}")  # True - a une population
168print(f"bool(extinct) ? {bool(extinct)}")  # False - population 0
169
170if lion:
171    print("Le lion a une population !")
172
173if not extinct:
174    print("Le dodo est éteint...")
175
176# Hash (__hash__) - fonctionne dans set et dict
177print("\n=== HASH - utilisation dans set/dict ===")
178species_set = {lion, elephant, rhino}
179print(f"Ensemble d'espèces : {len(species_set)} éléments")
180
181species_dict = {
182    lion: "Prédateur",
183    elephant: "Herbivore",
184    rhino: "Herbivore"
185}
186print(f"Type du lion : {species_dict[lion]}")

Liste des principales méthodes magiques

Initialisation et représentation

  • __init__(self, ...) - constructeur
  • __new__(cls, ...) - crée l'objet ; Python l'appelle avant __init__ (rarement redéfinie)
  • __str__(self) - chaîne pour l'utilisateur (print)
  • __repr__(self) - chaîne pour le programmeur (repr)
  • __format__(self, format_spec) - formatage (f"{obj:spec}")

Comparaisons

  • __eq__(self, other) - égalité ==
  • __ne__(self, other) - inégalité !=
  • __lt__(self, other) - inférieur <
  • __le__(self, other) - inférieur ou égal <=
  • __gt__(self, other) - supérieur >
  • __ge__(self, other) - supérieur ou égal >=

Arithmétique

  • __add__(self, other) - addition +
  • __sub__(self, other) - soustraction -
  • __mul__(self, other) - multiplication *
  • __truediv__(self, other) - division /
  • __floordiv__(self, other) - division entière //
  • __mod__(self, other) - modulo %
  • __pow__(self, other) - puissance **

Conteneur

  • __len__(self) - longueur len()
  • __getitem__(self, key) - lecture obj[key]
  • __setitem__(self, key, value) - écriture obj[key] = value
  • __delitem__(self, key) - suppression del obj[key]
  • __contains__(self, item) - appartenance item in obj
  • __iter__(self) - itérateur for x in obj
  • __next__(self) - élément suivant

Appelable et Gestionnaire de contexte

  • __call__(self, ...) - appel obj()
  • __enter__(self) - entrée with
  • __exit__(self, ...) - sortie with

Autres

  • __bool__(self) - conversion en bool
  • __hash__(self) - hash pour set/dict
  • __del__(self) - destructeur (rarement utilisé)

Résumé

Dans cette leçon, tu as appris :

  • Ce que sont les méthodes magiques (méthodes dunder)
  • Comment faire pour que tes classes se comportent comme les types intégrés
  • Implémenter __str__, __repr__, __eq__, __lt__
  • Les opérateurs arithmétiques (__add__, __sub__, __mul__)
  • Les comportements de conteneur (__len__, __getitem__, __contains__)
  • Les objets appelables avec __call__
  • Les gestionnaires de contexte avec __enter__ et __exit__
  • Un exemple concret avec un système de catalogage complet

Point de contrôle

Avant de passer à la suite :

  • Tu comprends la différence entre __str__ et __repr__
  • Tu sais implémenter des comparaisons
  • Tu sais ajouter des opérateurs arithmétiques
  • Tu comprends __len__, __getitem__, __iter__
  • Tu sais créer un objet appelable avec __call__

Analogie Safari : les méthodes magiques sont les instincts des animaux - un lion n'apprend pas à rugir, il le fait, tout simplement ! Tes classes peuvent aussi avoir des comportements naturels !

Dans la prochaine leçon, Darwin t'apprendra les type hints - comment décrire précisément les types de données pour rendre ton code plus lisible et plus sûr !

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelle est la différence entre __str__ et __repr__ ?

  2. 2. Quand la méthode __len__ est-elle appelée ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Tri vertical

    Range le cycle de vie d'un objet :

  • Éditeur de code

    Ajoute une méthode __str__ à la classe Animal qui renvoie 'Animal: {name}'

  • Éditeur de code

    Dans la classe Animal(name, species), ajoute une méthode __eq__ qui compare les animaux par espèce (même species = égaux)

  • Clique dans l'ordre

    Clique sur les éléments dans le bon ordre :

  • Clique dans l'ordre

    Clique sur les éléments de l'en-tête de la méthode appelée par print(objet) :

Articles utiles