Python & AI Tutorials Logo
Programmation Python

30. Introduction aux classes et aux objets

30.1) L’idée de la programmation orientée objet (POO) (créer vos propres types)

Tout au long de ce livre, vous avez travaillé avec les types intégrés de Python : entiers, chaînes, listes, dictionnaires, et plus encore. Chaque type regroupe des données (comme les caractères dans une chaîne) avec des opérations que vous pouvez effectuer sur ces données (comme .upper() ou .split()). Cette combinaison de données et de comportement est puissante : elle vous permet de penser aux chaînes comme à des entités complètes avec leurs propres capacités, et pas seulement comme à des séquences brutes de caractères.

La programmation orientée objet (POO) étend cette idée : elle vous permet de créer vos propres types personnalisés, appelés classes, qui regroupent des données et des comportements spécifiques à votre domaine de problème. Tout comme Python fournit un type str pour travailler avec du texte et un type list pour travailler avec des séquences, vous pouvez créer un type BankAccount pour gérer des transactions financières, un type Student pour suivre des dossiers scolaires, ou un type Product pour un système d’inventaire.

Pourquoi créer vos propres types ?

Imaginez que vous devez gérer des informations sur des étudiants dans un système scolaire. Sans classes, vous pourriez utiliser des variables séparées ou des dictionnaires :

python
# Utiliser des variables séparées - devient vite difficile à gérer
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
 
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
 
# Ou utiliser des dictionnaires - mieux, mais toujours limité
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}

Cette approche fonctionne pour des cas simples, mais elle a des limites :

  1. Aucune validation : rien ne vous empêche de définir gpa à une valeur invalide comme -5.0 ou "excellent"
  2. Aucun comportement lié : des opérations comme calculer un statut d’honneurs ou formater les informations d’un étudiant sont des fonctions séparées éparpillées dans votre code
  3. Aucune vérification de type : un dictionnaire représentant un étudiant ressemble à n’importe quel autre dictionnaire — Python ne peut pas vous aider à détecter des erreurs où vous utilisez par accident un dictionnaire de produit alors qu’un dictionnaire d’étudiant était attendu

Les classes résolvent ces problèmes en vous permettant de définir un nouveau type qui représente exactement ce qu’est un étudiant et quelles opérations ont du sens pour les étudiants :

python
# Nous allons construire vers ceci - une classe Student qui regroupe données et comportement
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def display_info(self):
        status = "Honors" if self.is_honors() else "Regular"
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
 
# Maintenant, nous pouvons créer des objets student
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.display_info())  # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors())       # Output: True

Ce chapitre va vous apprendre à construire des classes comme celle-ci à partir de zéro. Nous commencerons par les classes les plus simples possibles et ajouterons progressivement des fonctionnalités jusqu’à ce que vous puissiez créer des types personnalisés riches et utiles.

Classes vs instances : l’analogie du plan

Comprendre la distinction entre une classe et une instance est fondamental en programmation orientée objet :

  • Une classe est comme un plan ou un modèle. Elle définit quelles données un type d’objet va contenir et quelles opérations il peut effectuer. La classe elle-même n’est pas un étudiant spécifique : c’est la définition de ce que signifie être un étudiant.

  • Une instance (aussi appelée un objet) est un exemple spécifique créé à partir de ce plan. Quand vous créez alice = Student("Alice Johnson", "S12345", 3.8), vous créez une instance d’étudiant spécifique avec les données particulières d’Alice.

Classe Student
Plan

instance alice
Nom: Alice Johnson
ID: S12345
GPA: 3.8

instance bob
Nom: Bob Smith
ID: S12346
GPA: 3.5

instance carol
Nom: Carol Davis
ID: S12347
GPA: 3.9

Vous pouvez créer autant d’instances que vous le souhaitez à partir d’une seule classe, tout comme un architecte peut utiliser un plan pour construire de nombreuses maisons. Chaque instance a ses propres données (le GPA d’Alice est différent de celui de Bob), mais elles partagent toutes la même structure et les mêmes capacités définies par la classe.

Ce que vous apprendrez dans ce chapitre

Ce chapitre présente les concepts fondamentaux de la programmation orientée objet en Python :

  1. Définir des classes avec le mot-clé class
  2. Créer des instances et accéder à leurs attributs
  3. Ajouter des méthodes qui opèrent sur les données d’instance
  4. Comprendre self et comment les méthodes accèdent aux données de l’instance
  5. Initialiser des instances avec la méthode __init__
  6. Contrôler les représentations en chaîne avec __str__ et __repr__
  7. Créer plusieurs instances indépendantes à partir de la même classe

À la fin de ce chapitre, vous serez capable de concevoir et d’implémenter vos propres types personnalisés qui rendent vos programmes plus organisés, maintenables et expressifs. Nous nous appuierons sur ces bases au Chapitre 31 avec des fonctionnalités de classes plus avancées, et au Chapitre 32 avec l’héritage et le polymorphisme.

30.2) Définir des classes simples avec class

Commençons par créer la classe la plus simple possible — une classe qui définit juste un nouveau type, sans données ni comportement pour le moment.

Le mot-clé class

Vous définissez une classe en utilisant le mot-clé class, suivi du nom de la classe et de deux-points :

python
class Student:
    pass  # Classe vide pour le moment
 
# Créer une instance
alice = Student()
print(alice)  # Output: <__main__.Student object at 0x...>
print(type(alice))  # Output: <class '__main__.Student'>

Même cette classe minimale est utile : elle crée un nouveau type appelé Student. Quand vous créez une instance avec alice = Student(), Python crée un nouvel objet de type Student. La sortie montre que alice est bien un objet Student, même s’il ne fait encore rien d’intéressant.

Conventions de nommage des classes

Les noms de classes Python suivent une convention spécifique appelée CapWords ou PascalCase : chaque mot commence par une majuscule, sans underscores entre les mots :

python
class BankAccount:      # Bien : CapWords
    pass
 
class ProductInventory:  # Bien : CapWords
    pass
 
class HTTPRequest:      # Bien : les acronymes sont en majuscules
    pass
 
# Évitez ces styles pour les classes :
# class bank_account:   # Faux : snake_case est pour les fonctions/variables
# class bankaccount:    # Faux : difficile à lire
# class BANKACCOUNT:    # Faux : ALL_CAPS est pour les constantes

Cette convention aide à distinguer les classes des fonctions et des variables (qui utilisent snake_case) lors de la lecture du code.

Créer des instances

Créer une instance à partir d’une classe ressemble à l’appel d’une fonction : vous utilisez le nom de la classe suivi de parenthèses :

python
class Product:
    pass
 
# Créer trois instances de produit différentes
item1 = Product()
item2 = Product()
item3 = Product()
 
# Chaque instance est un objet séparé
print(item1)  # Output: <__main__.Product object at 0x...>
print(item2)  # Output: <__main__.Product object at 0x...>
print(item3)  # Output: <__main__.Product object at 0x...>
 
# Ce sont des objets différents, même s'ils sont du même type
print(item1 is item2)  # Output: False
print(type(item1) is type(item2))  # Output: True

Chaque appel à Product() crée une nouvelle instance indépendante. Les adresses mémoire (la partie 0x...) sont différentes, ce qui confirme qu’il s’agit d’objets séparés en mémoire.

Pourquoi commencer par des classes vides ?

Vous vous demandez peut-être pourquoi nous commençons avec des classes qui ne font rien. Il y a deux raisons :

  1. Clarté conceptuelle : comprendre qu’une classe est simplement un nouveau type, distinct de ses données et de son comportement, vous aide à saisir le concept fondamental avant d’ajouter de la complexité.

  2. Utilité pratique : même des classes vides peuvent être utiles comme marqueurs ou espaces réservés. Par exemple, vous pourriez définir des types d’exception personnalisés :

python
class InvalidGradeError:
    pass
 
class StudentNotFoundError:
    pass
 
# Ces classes vides servent de types d'erreur distincts

Cependant, les classes vides sont rares dans du code réel. Ajoutons maintenant des données pour rendre nos classes utiles.

30.3) Créer des instances et accéder aux attributs

Les classes deviennent utiles lorsqu’elles contiennent des données. En Python, vous pouvez ajouter des attributs (des données attachées à une instance) à tout moment en les affectant simplement.

Ajouter des attributs aux instances

Vous pouvez ajouter des attributs à une instance en utilisant la notation par point :

python
class Student:
    pass
 
# Créer une instance
alice = Student()
 
# Ajouter des attributs
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
 
# Accéder aux attributs
print(alice.name)        # Output: Alice Johnson
print(alice.student_id)  # Output: S12345
print(alice.gpa)         # Output: 3.8

L’opérateur point (.) accède aux attributs : alice.name signifie « obtenir l’attribut name de l’objet alice ». C’est la même syntaxe que vous utilisez avec les chaînes (comme text.upper()) et les listes (comme numbers.append(5)) : cela accède aux méthodes et aux attributs de ces objets.

Chaque instance a ses propres attributs

Différentes instances d’une même classe ont des attributs indépendants :

python
class Student:
    pass
 
# Créer deux étudiants
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Chaque instance a ses propres données
print(alice.name)  # Output: Alice Johnson
print(bob.name)    # Output: Bob Smith
 
# Modifier l'une n'affecte pas l'autre
alice.gpa = 3.9
print(alice.gpa)  # Output: 3.9
print(bob.gpa)    # Output: 3.5 (inchangé)

Cette indépendance est cruciale : alice et bob sont des objets distincts avec des données distinctes. Modifier alice.gpa n’affecte pas bob.gpa.

Les attributs peuvent être de n’importe quel type

Les attributs ne se limitent pas à des types simples : ils peuvent contenir n’importe quelle valeur Python :

python
class Student:
    pass
 
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90]  # Attribut de type liste
student.contact = {                 # Attribut de type dictionnaire
    "email": "carol@example.com",
    "phone": "555-0123"
}
student.is_active = True            # Attribut booléen
 
# Accéder aux données imbriquées
print(student.grades[0])           # Output: 95
print(student.contact["email"])    # Output: carol@example.com

Cette flexibilité vous permet de modéliser des entités complexes du monde réel avec des structures de données riches.

Accéder à des attributs inexistants

Essayer d’accéder à un attribut qui n’existe pas déclenche une AttributeError :

python
class Student:
    pass
 
student = Student()
student.name = "David Lee"
 
print(student.name)  # Output: David Lee
# print(student.age)  # AttributeError: 'Student' object has no attribute 'age'

Cette erreur est utile : elle détecte les fautes de frappe et les erreurs de logique lorsque vous vous attendez à ce qu’un attribut existe mais qu’il n’existe pas.

Le problème de l’affectation manuelle des attributs

Même si vous pouvez ajouter des attributs manuellement après avoir créé une instance, cette approche a des inconvénients importants :

python
class Student:
    pass
 
# Facile d'oublier des attributs ou de se tromper en les écrivant
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# Forgot to set gpa!
 
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346"  # Typo: stuent instead of student
bob.gpa = 3.5
 
# Maintenant alice n'a pas gpa, et bob a une faute de frappe
# print(alice.gpa)  # AttributeError
# print(bob.student_id)  # AttributeError

C’est source d’erreurs et fastidieux. Vous avez besoin d’un moyen de vous assurer que chaque instance démarre avec les bons attributs. C’est là qu’intervient la méthode __init__, que nous verrons en section 30.5. Mais d’abord, apprenons les méthodes : des fonctions qui appartiennent à une classe.

30.4) Ajouter des méthodes d’instance : comprendre self

Les méthodes sont des fonctions définies à l’intérieur d’une classe qui opèrent sur les données de l’instance. Elles donnent à vos classes un comportement, pas seulement des données.

Définir une méthode simple

Ajoutons une méthode à notre classe Student :

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Créer une instance et ajouter des attributs
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Appeler la méthode
alice.display_info()  # Output: Alice Johnson - GPA: 3.8

La méthode display_info est définie dans la classe avec def, exactement comme les fonctions classiques. La différence clé est le premier paramètre : self.

Comprendre self

Le paramètre self est la manière dont une méthode accède à l’instance spécifique sur laquelle elle opère. Quand vous appelez alice.display_info(), Python passe automatiquement alice comme premier argument à la méthode. À l’intérieur de la méthode, self fait référence à alice, donc self.name accède à alice.name et self.gpa accède à alice.gpa.

Voici ce qui se passe en coulisses :

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
# Ces deux appels sont équivalents :
alice.display_info()           # Façon normale
Student.display_info(alice)    # Ce que Python fait réellement
 
# Les deux affichent : Alice Johnson - GPA: 3.8

Quand vous écrivez alice.display_info(), Python le traduit en Student.display_info(alice). L’instance (alice) devient le paramètre self dans la méthode.

Pourquoi « self » ?

Le nom self est une convention, pas un mot-clé. Techniquement, vous pourriez utiliser n’importe quel nom :

python
class Student:
    def display_info(this):  # Fonctionne, mais ne faites pas ça
        print(f"{this.name} - GPA: {this.gpa}")

Cependant, utilisez toujours self. C’est une convention universelle en Python qui rend votre code lisible pour les autres programmeurs Python. Utiliser un autre nom va perturber les lecteurs et violer les standards de la communauté.

Méthodes avec plusieurs instances

La puissance de self devient claire quand vous avez plusieurs instances :

python
class Student:
    def display_info(self):
        print(f"{self.name} - GPA: {self.gpa}")
 
# Créer deux étudiants
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
 
# Même méthode, données différentes
alice.display_info()  # Output: Alice Johnson - GPA: 3.8
bob.display_info()    # Output: Bob Smith - GPA: 3.5

Quand vous appelez alice.display_info(), self est alice. Quand vous appelez bob.display_info(), self est bob. Le même code de méthode fonctionne pour n’importe quelle instance parce que self s’adapte à l’instance qui l’a appelée.

alice.display_info

self = alice

bob.display_info

self = bob

Accéder à alice.name
alice.gpa

Accéder à bob.name
bob.gpa

Les méthodes peuvent prendre des paramètres supplémentaires

Les méthodes peuvent accepter des paramètres au-delà de self :

python
class Student:
    def update_gpa(self, new_gpa):
        self.gpa = new_gpa
        print(f"Updated {self.name}'s GPA to {self.gpa}")
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
alice.update_gpa(3.9)  # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa)       # Output: 3.9

Quand vous appelez alice.update_gpa(3.9), Python passe alice comme self et 3.9 comme new_gpa. La signature de la méthode est def update_gpa(self, new_gpa), mais vous ne passez qu’un seul argument lors de l’appel : Python gère self automatiquement.

Les méthodes peuvent retourner des valeurs

Les méthodes peuvent retourner des valeurs comme les fonctions classiques :

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def get_status(self):
        if self.is_honors():
            return "Honors Student"
        else:
            return "Regular Student"
 
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
 
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
 
print(alice.get_status())  # Output: Honors Student
print(bob.get_status())    # Output: Regular Student

Remarquez comment get_status appelle une autre méthode (is_honors) via self.is_honors(). Les méthodes peuvent appeler d’autres méthodes sur la même instance.

Méthodes vs fonctions : quand utiliser l’une ou l’autre

Vous vous demandez peut-être quand utiliser une méthode plutôt qu’une fonction autonome. Voici la règle :

Utilisez une méthode quand l’opération :

  • A besoin d’accéder aux données de l’instance (self.name, self.gpa, etc.)
  • Appartient logiquement au type (c’est quelque chose qu’un Student fait ou est)
  • Modifie l’état de l’instance

Utilisez une fonction autonome quand l’opération :

  • N’a pas besoin des données de l’instance
  • Fonctionne avec plusieurs types
  • Est un utilitaire général
python
class Student:
    # Méthode : a besoin des données de l'instance
    def is_honors(self):
        return self.gpa >= 3.5
 
# Fonction : utilitaire général, fonctionne avec n'importe quelle valeur de GPA
def calculate_letter_grade(gpa):
    if gpa >= 3.7:
        return "A"
    elif gpa >= 3.0:
        return "B"
    elif gpa >= 2.0:
        return "C"
    else:
        return "D"
 
alice = Student()
alice.gpa = 3.8
 
# Utiliser la méthode pour des vérifications spécifiques à l'instance
print(alice.is_honors())  # Output: True
 
# Utiliser la fonction pour des calculs généraux
print(calculate_letter_grade(alice.gpa))  # Output: A
print(calculate_letter_grade(2.5))        # Output: C

Motifs courants de méthodes

Voici quelques motifs courants que vous utiliserez fréquemment :

Méthodes getter (récupérer des informations calculées) :

python
class Student:
    def get_full_info(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"

Méthodes setter (modifier des attributs avec validation) :

python
class Student:
    def set_gpa(self, new_gpa):
        if 0.0 <= new_gpa <= 4.0:
            self.gpa = new_gpa
        else:
            print("Invalid GPA: must be between 0.0 and 4.0")

Méthodes de requête (répondre à des questions oui/non) :

python
class Student:
    def is_honors(self):
        return self.gpa >= 3.5
    
    def is_failing(self):
        return self.gpa < 2.0

Méthodes d’action (effectuer des opérations) :

python
class Student:
    def add_grade(self, grade):
        self.grades.append(grade)
        # Recalculer le GPA en fonction de toutes les notes
        self.gpa = sum(self.grades) / len(self.grades)

30.5) Initialiser des instances avec __init__

Définir manuellement des attributs après la création d’une instance est fastidieux et source d’erreurs. La méthode __init__ résout cela en vous permettant d’initialiser les instances avec des données au moment où elles sont créées.

La méthode __init__

La méthode __init__ (prononcée « dunder init » ou « init ») est une méthode spéciale que Python appelle automatiquement quand vous créez une nouvelle instance :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Créer des instances avec des données initiales
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
 
print(alice.name)  # Output: Alice Johnson
print(bob.gpa)     # Output: 3.5

Quand vous écrivez Student("Alice Johnson", "S12345", 3.8), Python :

  1. Crée une nouvelle instance Student vide
  2. Appelle __init__ avec cette instance comme self et vos arguments
  3. Retourne l’instance initialisée

La méthode __init__ ne retourne pas explicitement de valeur : elle modifie l’instance sur place en définissant ses attributs. Si vous essayez de retourner une valeur depuis __init__, Python lèvera une TypeError.

python
class Student:
    def __init__(self, name):
        self.name = name
        # Ne retournez rien depuis __init__
        # return self  # Wrong! TypeError: __init__() should return None, not 'Student'

Comment __init__ fonctionne

Décortiquons ce qui se passe étape par étape :

python
class Student:
    def __init__(self, name, student_id, gpa):
        print(f"Initializing student: {name}")
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
        print(f"Initialization complete")
 
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
 
print(alice.name)  # Output: Alice Johnson

Les paramètres après self (name, student_id, gpa) deviennent des arguments obligatoires lors de la création d’une instance. Si vous ne les fournissez pas, Python lève une TypeError :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# student = Student()  # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice")  # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8)  # Correct

C’est bien mieux que l’affectation manuelle des attributs : Python impose que chaque instance démarre avec les données requises.

Valeurs de paramètres par défaut dans __init__

Vous pouvez utiliser des valeurs de paramètres par défaut dans __init__, comme dans les fonctions classiques :

python
class Student:
    def __init__(self, name, student_id, gpa=0.0):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
# Le GPA est optionnel, par défaut à 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346")  # Utilise gpa=0.0 par défaut
 
print(alice.gpa)  # Output: 3.8
print(bob.gpa)    # Output: 0.0

C’est utile pour des attributs qui ont des valeurs par défaut raisonnables, mais qui peuvent être personnalisés si nécessaire.

Validation dans __init__

Vous pouvez valider les entrées dans __init__ pour vous assurer que les instances démarrent dans un état valide :

python
class Student:
    def __init__(self, name, student_id, gpa):
        if not name:
            print("Error: Name cannot be empty")
            self.name = "Unknown"
        else:
            self.name = name
        
        self.student_id = student_id
        
        if 0.0 <= gpa <= 4.0:
            self.gpa = gpa
        else:
            print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
            self.gpa = 0.0
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa)  # Output: 3.8
 
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name)  # Output: Unknown
print(bob.gpa)   # Output: 0.0

Cela garantit que même si quelqu’un passe des données invalides, l’instance se retrouve dans un état raisonnable.

30.6) Représentations en chaîne avec __str__ et __repr__

Quand vous affichez une instance avec print() ou que vous la visualisez dans le shell interactif, Python doit la convertir en chaîne. Par défaut, vous obtenez quelque chose de peu utile :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: <__main__.Student object at 0x...>

La sortie par défaut affiche le nom de la classe et l’adresse mémoire, mais rien sur les données réelles d’Alice. Vous pouvez personnaliser cela avec les méthodes spéciales __str__ et __repr__.

La méthode __str__

La méthode __str__ définit comment vos instances sont converties en chaînes par print() et str() :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice)  # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice))  # Output: Alice Johnson (S12345) - GPA: 3.8

La méthode __str__ devrait retourner une chaîne lisible et informative pour les utilisateurs finaux. Voyez-la comme la représentation « conviviale ».

La méthode __repr__

La méthode __repr__ définit la représentation en chaîne « officielle » de vos instances, utilisée par le REPL et repr() :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice))  # Output: Student('Alice Johnson', 'S12345', 3.8)

Dans le REPL :

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)

La méthode __repr__ devrait retourner une chaîne qui ressemble à du code Python valide pour recréer l’objet. Voyez-la comme la représentation « développeur » : elle doit être non ambiguë et utile pour le débogage.

Utiliser __str__ et __repr__ ensemble

Vous pouvez définir les deux méthodes pour des objectifs différents :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def __str__(self):
        # Format convivial, lisible
        return f"{self.name} - GPA: {self.gpa}"
    
    def __repr__(self):
        # Format non ambigu, proche du code
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
alice = Student("Alice Johnson", "S12345", 3.8)
 
print(alice)        # Utilise __str__
# Output: Alice Johnson - GPA: 3.8
 
print(repr(alice))  # Utilise __repr__
# Output: Student('Alice Johnson', 'S12345', 3.8)

Dans le REPL :

python
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice  # Uses __repr__
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice)  # Uses __str__
Alice Johnson - GPA: 3.8

Quand définir quelle méthode

Voici la règle :

  • Définissez toujours __repr__ : elle est utilisée par le REPL et les outils de débogage. Si vous n’en définissez qu’une, définissez celle-ci.
  • Définissez __str__ lorsque vous avez besoin d’un format convivial : si votre classe sera affichée pour des utilisateurs finaux, fournissez un __str__ lisible.
  • Si vous ne définissez que __repr__ : Python l’utilise pour repr(), et str() se rabat aussi sur __repr__ (donc print() l’utilise aussi).
  • Si vous ne définissez que __str__ : print() utilise __str__, mais repr() et le REPL utilisent le __repr__ par défaut (affichant l’adresse mémoire). C’est pourquoi définir __repr__ est généralement plus important.
python
# Seulement __repr__ défini
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
 
item = Product("Laptop", 999.99)
print(item)        # Utilise __repr__ comme solution de repli
# Output: Product('Laptop', 999.99)
print(repr(item))  # Utilise __repr__
# Output: Product('Laptop', 999.99)

Représentation en chaîne dans les collections

Quand des instances sont dans des collections (listes, dicts, etc.), Python utilise __repr__ pour les afficher, pas __str__ :

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
    
    def __str__(self):
        return f"{self.name}: {self.gpa}"
    
    def __repr__(self):
        return f"Student('{self.name}', {self.gpa})"
 
students = [
    Student("Alice", 3.8),
    Student("Bob", 3.5),
    Student("Carol", 3.9)
]
 
# Afficher la liste utilise __repr__ pour chaque étudiant
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
 
# Afficher des étudiants individuellement utilise __str__
for student in students:
    print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9

C’est pourquoi __repr__ devrait être non ambiguë : cela vous aide à comprendre ce qu’il y a dans vos structures de données pendant le débogage. Quand vous imprimez une liste, Python appelle essentiellement repr() sur chaque élément pour afficher la structure clairement.

30.7) Créer plusieurs instances indépendantes

L’un des aspects les plus puissants des classes est que vous pouvez créer de nombreuses instances indépendantes, chacune avec ses propres données. Explorons cela en profondeur.

Chaque instance a ses propres données

Quand vous créez plusieurs instances à partir de la même classe, chacune conserve ses propres attributs distincts :

python
class BankAccount:
    def __init__(self, account_number, holder_name, balance=0.0):
        self.account_number = account_number
        self.holder_name = holder_name
        self.balance = balance
    
    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
    
    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
            return True
        else:
            print(f"Insufficient funds. Balance: ${self.balance:.2f}")
            return False
    
    def __str__(self):
        return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
 
# Créer trois comptes indépendants
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
 
# Les opérations sur un compte n'affectent pas les autres
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
 
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
 
# Chaque compte conserve son propre solde
print(alice_account)  # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account)    # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account)  # Output: Carol Davis's account (ACC-003): $2000.00

Cette indépendance est fondamentale en programmation orientée objet. Chaque instance est une entité séparée avec son propre état.

Instances dans des collections

Vous pouvez stocker des instances dans des listes, des dictionnaires, ou toute autre collection :

python
class Student:
    def __init__(self, name, student_id, gpa):
        self.name = name
        self.student_id = student_id
        self.gpa = gpa
    
    def is_honors(self):
        return self.gpa >= 3.5
    
    def __repr__(self):
        return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
 
# Créer une liste d'étudiants
students = [
    Student("Alice Johnson", "S12345", 3.8),
    Student("Bob Smith", "S12346", 3.2),
    Student("Carol Davis", "S12347", 3.9),
    Student("David Lee", "S12348", 3.4)
]
 
# Trouver tous les étudiants avec honneurs
honors_students = []
for student in students:
    if student.is_honors():
        honors_students.append(student)
 
print("Honors students:")
for student in honors_students:
    print(f"  {student.name}: {student.gpa}")
# Output:
# Honors students:
#   Alice Johnson: 3.8
#   Carol Davis: 3.9
 
# Calculer le GPA moyen
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}")  # Output: Average GPA: 3.58

C’est un motif courant : créer plusieurs instances, les stocker dans une collection, puis les traiter avec des boucles et des compréhensions.

Les instances peuvent référencer d’autres instances

Les instances peuvent avoir des attributs qui référencent d’autres instances, créant des relations entre objets :

python
class Course:
    def __init__(self, course_code, course_name):
        self.course_code = course_code
        self.course_name = course_name
    
    def __str__(self):
        return f"{self.course_code}: {self.course_name}"
 
class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
        self.courses = []  # Liste d'instances Course
    
    def enroll(self, course):
        self.courses.append(course)
        print(f"{self.name} enrolled in {course.course_name}")
    
    def list_courses(self):
        print(f"{self.name}'s courses:")
        for course in self.courses:
            print(f"  {course}")
 
# Créer des cours
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
 
# Créer des étudiants et les inscrire à des cours
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
 
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
 
# Lister les cours de chaque étudiant
alice.list_courses()
# Output:
# Alice Johnson's courses:
#   CS101: Introduction to Python
#   CS102: Data Structures
 
bob.list_courses()
# Output:
# Bob Smith's courses:
#   CS101: Introduction to Python
#   CS103: Web Development

Remarquez qu’Alice et Bob sont tous deux inscrits à python_course : ils référencent la même instance de Course. Cela modélise la relation du monde réel où plusieurs étudiants peuvent suivre le même cours.

Identité et égalité des instances

Chaque instance est un objet unique, même si elle contient les mêmes données qu’une autre instance :

python
class Student:
    def __init__(self, name, gpa):
        self.name = name
        self.gpa = gpa
 
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
 
# Objets différents, même avec des données identiques
print(alice1 is alice2)  # Output: False
print(id(alice1) == id(alice2))  # Output: False

Par défaut, == vérifie aussi l’identité (s’il s’agit du même objet), et pas si les données sont identiques. Au Chapitre 31, nous apprendrons à personnaliser la comparaison d’égalité avec la méthode spéciale __eq__.


Ce chapitre vous a présenté les fondamentaux de la programmation orientée objet en Python. Vous avez appris à définir des classes, créer des instances, ajouter des méthodes, initialiser des instances avec __init__, contrôler les représentations en chaîne, et travailler avec plusieurs instances indépendantes. Ces concepts constituent la base de fonctionnalités POO plus avancées que nous explorerons dans les Chapitres 31 et 32.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai