31. Fonctionnalités avancées des classes
Dans le chapitre 30, nous avons appris à créer des classes de base avec des attributs d’instance et des méthodes. Maintenant, nous allons explorer des fonctionnalités de classe plus sophistiquées qui vous donnent un contrôle fin sur la manière dont vos objets se comportent. Ces fonctionnalités vous permettent de créer des classes qui ressemblent aux types intégrés de Python, avec une syntaxe naturelle pour des opérations comme l’addition, la comparaison et l’indexation.
31.1) Variables de classe vs variables d’instance
Quand nous créons des attributs dans une classe, nous avons deux emplacements fondamentalement différents pour les stocker : sur la classe elle-même ou sur des instances individuelles. Comprendre cette distinction est crucial pour écrire du code orienté objet correct.
31.1.1) Comprendre les variables d’instance
Les variables d’instance(instance variables) sont des attributs qui appartiennent à un objet spécifique. Chaque instance possède sa propre copie séparée de ces variables. Nous avons utilisé des variables d’instance tout au long du chapitre 30 : ce sont les attributs que nous créons dans __init__ en utilisant self :
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Variable d’instance
self.balance = balance # Variable d’instance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.balance) # Output: 1000
print(account2.balance) # Output: 500Chaque instance de BankAccount a son propre owner et son propre balance. Modifier account1.balance n’affecte pas account2.balance : ils sont complètement indépendants.
31.1.2) Comprendre les variables de classe
Les variables de classe(class variables) sont des attributs qui appartiennent à la classe elle-même, et non à une instance en particulier. Toutes les instances partagent la même variable de classe. Nous définissons les variables de classe directement dans le corps de la classe, en dehors de toute méthode :
class BankAccount:
interest_rate = 0.02 # Variable de classe - partagée par toutes les instances
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rate
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
print(BankAccount.interest_rate) # Output: 0.02Remarquez que nous pouvons accéder à interest_rate via les instances (account1.interest_rate) ou via la classe elle-même (BankAccount.interest_rate). Les deux font référence à la même variable.
Voici ce qui rend les variables de classe puissantes : quand nous modifions la variable de classe, toutes les instances voient le changement :
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
# Modifier la variable de classe
BankAccount.interest_rate = 0.03
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03Les deux instances voient immédiatement le nouveau taux d’intérêt, car elles consultent toutes la même variable de classe.
31.1.3) Le piège du masquage : quand des variables d’instance cachent des variables de classe
Voici un comportement subtil mais important : si vous assignez un attribut via une instance, Python crée une variable d’instance qui masque (cache) la variable de classe :
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
# Créer une variable d’instance qui masque la variable de classe
account1.interest_rate = 0.05
print(account1.interest_rate) # Output: 0.05 (instance variable)
print(account2.interest_rate) # Output: 0.02 (class variable)
print(BankAccount.interest_rate) # Output: 0.02 (class variable)Maintenant, account1 a sa propre variable d’instance interest_rate qui masque la variable de classe. La variable de classe existe toujours, mais account1.interest_rate fait référence à la variable d’instance à la place. Ce n’est généralement pas ce que vous voulez : si vous devez modifier une variable de classe, modifiez-la via le nom de la classe, pas via une instance.
31.1.4) Utilisations pratiques des variables de classe
Les variables de classe sont utiles pour des données qui doivent être partagées entre toutes les instances :
class Student:
school_name = "Python High School" # Identique pour tous les élèves
total_students = 0 # Suivre combien d’élèves existent
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.total_students += 1 # Incrémenter lors de la création d’un élève
def __str__(self):
return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
print(student1) # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}") # Output: Total students: 3Remarquez comment nous utilisons Student.total_students (et non self.total_students) dans __init__ afin de montrer clairement que nous modifions la variable de classe, et non que nous créons une variable d’instance.
31.2) Gérer les attributs avec @property
Parfois, vous voulez contrôler ce qui se passe quand quelqu’un accède à un attribut ou le modifie. Par exemple, vous pourriez vouloir valider qu’une valeur est positive, ou calculer une valeur à la volée plutôt que de la stocker. Le décorateur @property de Python vous permet d’écrire des méthodes qui ressemblent à un simple accès à un attribut.
31.2.1) Le problème : l’accès direct aux attributs ne peut pas valider
Quand les attributs sont accessibles directement, vous ne pouvez pas valider ou transformer les valeurs :
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
temp = Temperature(25)
print(temp.celsius) # Output: 25
# Rien ne nous empêche de définir des températures physiquement impossibles
temp.celsius = -500 # En dessous du zéro absolu (-273.15°C) !
print(temp.celsius) # Output: -500
# Ou des valeurs absurdement élevées
temp.celsius = 1000000
print(temp.celsius) # Output: 1000000Sans validation, nous pouvons définir accidentellement des données invalides, ce qui conduit à des bugs plus tard dans le programme. Nous pourrions utiliser des méthodes comme get_celsius() et set_celsius(), mais ce n’est pas idiomatique en Python. Les développeurs Python s’attendent à accéder aux attributs directement, pas via des méthodes getter/setter comme en Java ou C++.
31.2.2) Utiliser @property pour des attributs calculés
Le décorateur @property transforme une méthode en « getter » accessible comme un attribut :
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
"""Convert celsius to fahrenheit on-the-fly"""
return self.celsius * 9/5 + 32
temp = Temperature(25)
print(temp.celsius) # Output: 25
print(temp.fahrenheit) # Output: 77.0 (computed, not stored)Remarquez que nous appelons temp.fahrenheit sans parenthèses : cela ressemble à un accès à un attribut, mais cela appelle en réalité la méthode. La valeur en fahrenheit est calculée « à la volée » à chaque accès ; elle n’est pas stockée, donc elle reste toujours synchronisée avec celsius :
temp = Temperature(0)
print(temp.fahrenheit) # Output: 32.0
temp.celsius = 100
print(temp.fahrenheit) # Output: 212.0 (automatically updated)31.2.3) Ajouter un setter avec @property_name.setter
Pour autoriser la définition d’une propriété, nous ajoutons une méthode setter en utilisant le décorateur @property_name.setter :
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Convert fahrenheit to celsius when setting"""
self.celsius = (value - 32) * 5/9
temp = Temperature(0)
print(temp.celsius) # Output: 0
print(temp.fahrenheit) # Output: 32.0
# Définir la température en utilisant fahrenheit
temp.fahrenheit = 212
print(temp.celsius) # Output: 100.0
print(temp.fahrenheit) # Output: 212.0La méthode setter reçoit la nouvelle valeur et peut la valider ou la transformer avant de la stocker.
31.2.4) Utiliser des propriétés pour la validation
Les propriétés sont excellentes pour imposer des contraintes :
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # Le soulignement suggère un "usage interne"
@property
def balance(self):
"""Get the current balance"""
return self._balance
@balance.setter
def balance(self, value):
"""Set balance, but only if non-negative"""
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
account = BankAccount("Alice", 1000)
print(account.balance) # Output: 1000
account.balance = 1500 # Fonctionne très bien
print(account.balance) # Output: 1500
# Ceci déclenche une erreur
account.balance = -100
# Output: ValueError: Balance cannot be negativeRemarquez la convention de nommage : nous stockons la valeur réelle dans _balance (avec un underscore initial) et nous l’exposons via la propriété balance. L’underscore est une convention Python suggérant « ceci est un détail d’implémentation interne », même si l’attribut reste techniquement accessible. Ce schéma nous permet de contrôler l’accès via la propriété tout en gardant le stockage réel séparé.
31.2.5) Propriétés en lecture seule
Si vous définissez une propriété sans setter, elle devient en lecture seule :
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""Computed read-only property"""
return self.width * self.height
rect = Rectangle(5, 3)
print(rect.area) # Output: 15
rect.width = 10
print(rect.area) # Output: 30 (automatically updated)
# Essayer de définir area déclenche une erreur
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setterC’est utile pour des valeurs dérivées qui doivent être calculées, pas stockées.
31.3) Méthodes de classe avec @classmethod
Parfois, vous avez besoin de méthodes qui travaillent avec la classe elle-même plutôt qu’avec des instances. Les méthodes de classe(class methods) reçoivent la classe comme premier argument (conventionnellement nommé cls) au lieu d’une instance (self).
31.3.1) Définir des méthodes de classe
Nous créons des méthodes de classe avec le décorateur @classmethod :
class Student:
school_name = "Python High School"
def __init__(self, name, grade):
self.name = name
self.grade = grade
@classmethod
def get_school_name(cls):
"""Class method - receives the class, not an instance"""
return cls.school_name
# Appel sur la classe elle-même
print(Student.get_school_name()) # Output: Python High School
# Peut aussi être appelée sur une instance (mais cls est toujours la classe)
student = Student("Alice", 10)
print(student.get_school_name()) # Output: Python High SchoolLe paramètre cls reçoit automatiquement la classe, tout comme self reçoit automatiquement l’instance dans les méthodes classiques.
31.3.2) Constructeurs alternatifs avec des méthodes de classe
L’une des utilisations les plus courantes des méthodes de classe consiste à créer des constructeurs alternatifs : différentes manières de créer des instances :
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Create a Date from a string like '2024-12-27'"""
year, month, day = date_string.split('-')
return cls(int(year), int(month), int(day))
@classmethod
def today(cls):
"""Create a Date for today (simplified example)"""
# In real code, you'd use the datetime module
return cls(2024, 12, 27)
def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Constructeur normal
date1 = Date(2024, 12, 27)
print(date1) # Output: 2024-12-27
# Constructeur alternatif depuis une chaîne
date2 = Date.from_string("2024-12-27")
print(date2) # Output: 2024-12-27
# Constructeur alternatif pour aujourd’hui
date3 = Date.today()
print(date3) # Output: 2024-12-27Remarquez comment from_string et today renvoient tous deux cls(...) : cela crée une nouvelle instance de la classe. Utiliser cls au lieu de coder en dur Date permet au code de fonctionner correctement avec des sous-classes (nous apprendrons l’héritage dans le chapitre 32).
31.3.3) Méthodes de classe pour des patrons factory
Les méthodes de classe sont utiles pour créer des instances avec différentes configurations :
class DatabaseConnection:
def __init__(self, host, port, database, username):
self.host = host
self.port = port
self.database = database
self.username = username
@classmethod
def for_development(cls):
"""Create a connection configured for development"""
return cls("localhost", 5432, "dev_db", "dev_user")
@classmethod
def for_production(cls):
"""Create a connection configured for production"""
return cls("prod.example.com", 5432, "prod_db", "prod_user")
def __str__(self):
return f"Connection to {self.database} at {self.host}:{self.port}"
# Facile de créer des connexions préconfigurées
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
print(dev_conn) # Output: Connection to dev_db at localhost:5432
print(prod_conn) # Output: Connection to prod_db at prod.example.com:543231.3.4) Méthodes de classe pour compter les instances
Les méthodes de classe peuvent travailler avec des variables de classe pour suivre des informations concernant toutes les instances :
class Product:
total_products = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.total_products += 1
@classmethod
def get_total_products(cls):
"""Return the total number of products created"""
return cls.total_products
@classmethod
def reset_count(cls):
"""Reset the product counter"""
cls.total_products = 0
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
print(Product.get_total_products()) # Output: 3
Product.reset_count()
print(Product.get_total_products()) # Output: 031.4) Méthodes statiques avec @staticmethod
Les méthodes statiques(static methods) sont des méthodes qui ne reçoivent ni l’instance (self) ni la classe (cls) comme premier argument. Ce sont simplement des fonctions normales définies à l’intérieur d’une classe, parce qu’elles sont logiquement liées à cette classe.
31.4.1) Définir des méthodes statiques
Nous créons des méthodes statiques avec le décorateur @staticmethod :
class MathUtils:
@staticmethod
def is_even(number):
"""Check if a number is even"""
return number % 2 == 0
@staticmethod
def is_prime(number):
"""Check if a number is prime (simplified)"""
if number < 2:
return False
for i in range(2, int(number ** 0.5) + 1):
if number % i == 0:
return False
return True
# Appeler des méthodes statiques sur la classe
print(MathUtils.is_even(4)) # Output: True
print(MathUtils.is_even(7)) # Output: False
print(MathUtils.is_prime(17)) # Output: True
print(MathUtils.is_prime(18)) # Output: False
# Peut aussi être appelée sur une instance (mais c’est la même fonction)
utils = MathUtils()
print(utils.is_even(10)) # Output: TrueLes méthodes statiques n’ont pas besoin d’accéder aux données de l’instance ou de la classe : ce sont des fonctions utilitaires autonomes.
31.4.2) Quand utiliser des méthodes statiques vs des méthodes de classe vs des méthodes d’instance
Voici comment choisir :
class Temperature:
# Variable de classe
absolute_zero_celsius = -273.15
def __init__(self, celsius):
self.celsius = celsius
# Méthode d’instance - a besoin d’accéder aux données de l’instance (self)
def to_fahrenheit(self):
return self.celsius * 9/5 + 32
# Méthode de classe - a besoin d’accéder aux données de la classe (cls)
@classmethod
def get_absolute_zero(cls):
return cls.absolute_zero_celsius
# Méthode statique - n’a pas besoin des données de l’instance ou de la classe
@staticmethod
def celsius_to_kelvin(celsius):
return celsius + 273.15
@staticmethod
def fahrenheit_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5/9
temp = Temperature(25)
# Méthode d’instance - utilise les données de l’instance
print(temp.to_fahrenheit()) # Output: 77.0
# Méthode de classe - utilise les données de la classe
print(Temperature.get_absolute_zero()) # Output: -273.15
# Méthodes statiques - juste des fonctions utilitaires
print(Temperature.celsius_to_kelvin(25)) # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77)) # Output: 25.0Directives :
- Utilisez des méthodes d’instance(instance methods) quand vous avez besoin d’accéder aux attributs d’instance (
self) - Utilisez des méthodes de classe(class methods) quand vous avez besoin d’accéder aux attributs de classe ou que vous voulez des constructeurs alternatifs (
cls) - Utilisez des méthodes statiques(static methods) quand vous n’avez pas besoin d’accéder aux données de l’instance ou de la classe, mais que la fonction est logiquement liée à la classe
Note : Les méthodes statiques pourraient être des fonctions autonomes, mais les placer dans la classe regroupe les fonctionnalités liées ensemble et évite d’encombrer l’espace de noms global.
| Type de méthode | Premier paramètre | À utiliser quand |
|---|---|---|
| Méthode d’instance | self | Besoin d’accéder aux données de l’instance |
| Méthode de classe | cls | Besoin d’accéder aux données de la classe ou de constructeurs alternatifs |
| Méthode statique | (aucun) | Fonction utilitaire liée à la classe |
31.4.3) Exemple pratique : utilitaires de validation
Les méthodes statiques sont idéales pour la validation et les fonctions utilitaires :
class User:
def __init__(self, username, password):
if not User.is_valid_username(username):
raise ValueError("Invalid username")
if not User.is_valid_password(password):
raise ValueError("Invalid password")
self.username = username
self._password = password
@staticmethod
def is_valid_username(username):
"""Check if username meets requirements"""
return len(username) >= 3 and username.isalnum()
@staticmethod
def is_valid_password(password):
"""Check if password meets security requirements"""
return len(password) >= 8 and any(c.isdigit() for c in password)
# Ces méthodes de validation peuvent être utilisées indépendamment
print(User.is_valid_username("alice123")) # Output: True
print(User.is_valid_username("ab")) # Output: False
print(User.is_valid_password("pass1234")) # Output: True
# Et elles peuvent être utilisées dans n’importe quelle méthode de la classe
try:
user = User("ab", "short")
except ValueError as e:
print(f"Error: {e}") # Output: Error: Invalid username31.5) Comprendre les méthodes spéciales (méthodes magiques)
Les méthodes spéciales(special methods) (aussi appelées méthodes magiques(magic methods) ou méthodes dunder(dunder methods) parce qu’elles ont des doubles underscores) vous permettent de personnaliser le comportement de vos objets avec les opérations intégrées de Python. Nous avons déjà utilisé __init__, __str__ et __repr__ dans le chapitre 30. Maintenant, nous allons en explorer beaucoup d’autres.
31.5.1) Ce que font les méthodes spéciales
Les méthodes spéciales sont appelées automatiquement par Python quand vous utilisez une certaine syntaxe ou des fonctions intégrées :
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
point = Point(3, 4)
# Quand vous appelez print(), Python appelle __str__()
print(point) # Output: Point(3, 4)
# Ceci équivaut à : print(point.__str__())Les méthodes spéciales vous permettent de faire en sorte que vos classes se comportent comme des types intégrés. Par exemple, vous pouvez faire en sorte que vos objets :
- Prennent en charge les opérations arithmétiques (
+,-,*,/) - Soient comparables (
<,>,==) - Fonctionnent avec
len(),inet l’indexation - Agissent comme des conteneurs ou des séquences
31.5.2) Catégories courantes de méthodes spéciales
Voici les principales catégories de méthodes spéciales :
Représentation en chaîne (comment les objets sont affichés) :
__str__()- pourprint()etstr()__repr__()- pour le REPL etrepr()
Comparaison (comparer des objets) :
__eq__()- pour==__ne__()- pour!=__lt__()- pour<__le__()- pour<=__gt__()- pour>__ge__()- pour>=
Arithmétique (opérations mathématiques) :
__add__()- pour+__sub__()- pour-__mul__()- pour*__truediv__()- pour/
Conteneur/Séquence (comportement de type collection) :
__len__()- pourlen()__contains__()- pourin__getitem__()- pour l’indexationobj[key]__setitem__()- pour l’affectationobj[key] = value
Nous allons les explorer en détail dans les sections suivantes.
31.6) Exemple 1 : interface de collection (len, contains)
Créons une classe qui gère une collection d’éléments et faisons-la fonctionner avec la fonction intégrée len() de Python et l’opérateur in.
31.6.1) Implémenter len pour len()
La méthode spéciale __len__() est appelée quand vous utilisez len() sur votre objet :
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
"""Return the number of items in the cart"""
return len(self.items)
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
# len() appelle __len__()
print(len(cart)) # Output: 3Sans __len__(), appeler len(cart) déclencherait un TypeError. En l’implémentant, notre ShoppingCart fonctionne comme les collections intégrées.
31.6.2) Implémenter contains pour l’opérateur in
La méthode spéciale __contains__() est appelée quand vous utilisez l’opérateur in :
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __contains__(self, item):
"""Check if an item is in the cart"""
return item in self.items
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
# l’opérateur in appelle __contains__()
print("Apple" in cart) # Output: True
print("Orange" in cart) # Output: FalseMaintenant, notre panier prend en charge la syntaxe Python naturelle pour tester l’appartenance.
31.6.3) Construire une classe de collection plus complète
Créons une classe de collection plus réaliste qui suit les notes des élèves :
class GradeBook:
def __init__(self):
self.grades = {} # student_name: list of grades
def add_grade(self, student, grade):
"""Add a grade for a student"""
if student not in self.grades:
self.grades[student] = []
self.grades[student].append(grade)
def __len__(self):
"""Return the number of students"""
return len(self.grades)
def __contains__(self, student):
"""Check if a student has any grades"""
return student in self.grades
def get_average(self, student):
"""Get a student's average grade"""
if student not in self:
return None
grades = self.grades[student]
return sum(grades) / len(grades)
def __str__(self):
return f"GradeBook with {len(self)} students"
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
print(gradebook) # Output: GradeBook with 2 students
print(len(gradebook)) # Output: 2
print("Alice" in gradebook) # Output: True
print("Carol" in gradebook) # Output: False
print(f"Alice's average: {gradebook.get_average('Alice')}") # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}") # Output: Bob's average: 82.66666666666667Remarquez comment get_average() utilise if student not in self : cela appelle notre méthode __contains__(), ce qui rend le code naturel à lire.
31.7) Exemple 2 : surcharge d’opérateurs (add, eq, lt)
La surcharge d’opérateurs(operator overloading) signifie définir ce que des opérateurs comme +, == et < font pour vos classes personnalisées. Cela permet à vos objets de fonctionner naturellement avec la syntaxe Python.
31.7.1) Implémenter add pour l’addition
La méthode spéciale __add__() est appelée quand vous utilisez l’opérateur + :
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Add two vectors"""
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# l’opérateur + appelle __add__()
v3 = v1 + v2
print(v3) # Output: Vector(4, 6)Quand Python voit v1 + v2, il appelle v1.__add__(v2). La méthode __add__() de l’opérande de gauche reçoit l’opérande de droite comme argument.
31.7.2) Implémenter eq pour l’égalité
La méthode spéciale __eq__() est appelée quand vous utilisez l’opérateur == :
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""Check if two vectors are equal"""
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
# l’opérateur == appelle __eq__()
print(v1 == v2) # Output: True
print(v1 == v3) # Output: FalseSans __eq__(), Python compare l’identité des objets (s’il s’agit du même objet en mémoire), et non leurs valeurs. Avec __eq__(), nous définissons ce que l’égalité signifie pour notre classe.
31.7.3) Implémenter les opérateurs de comparaison
Implémentons des opérateurs de comparaison pour une classe Money :
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
"""Check if amounts are equal"""
return self.amount == other.amount
def __lt__(self, other):
"""Check if this amount is less than other"""
return self.amount < other.amount
def __le__(self, other):
"""Check if this amount is less than or equal to other"""
return self.amount <= other.amount
def __gt__(self, other):
"""Check if this amount is greater than other"""
return self.amount > other.amount
def __ge__(self, other):
"""Check if this amount is greater than or equal to other"""
return self.amount >= other.amount
def __str__(self):
return f"${self.amount:.2f}"
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
print(price1 == price3) # Output: True
print(price1 < price2) # Output: True
print(price1 <= price3) # Output: True
print(price2 > price1) # Output: True
print(price2 >= price1) # Output: True31.7.4) Gérer les incompatibilités de type dans les opérateurs
Lors de l’implémentation des opérateurs, vous devriez gérer les cas où l’autre opérande n’est pas du type attendu :
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Add two vectors or add a scalar to both components"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
elif isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
else:
return NotImplemented # Laisser Python essayer other.__radd__(self)
def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Output: Vector(4, 6) (vector addition)
print(v1 + 5) # Output: Vector(6, 7) (scalar addition)
print(v1 == v2) # Output: False
print(v1 == "not a vector") # Output: False (no error)Renvoyer NotImplemented (une constante intégrée spéciale) indique à Python d’essayer l’opération réfléchie sur l’autre opérande. C’est important pour que les opérateurs fonctionnent correctement avec différents types.
31.8) Exemple 3 : accès de type séquence (getitem, setitem)
Les méthodes spéciales __getitem__() et __setitem__() vous permettent d’utiliser la syntaxe d’indexation (obj[key]) avec vos classes personnalisées. Cela fait se comporter vos objets comme des listes, des dictionnaires ou d’autres séquences.
31.8.1) Implémenter getitem pour l’indexation
La méthode __getitem__() est appelée quand vous utilisez des crochets pour accéder à un élément :
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Get a song by index"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# L’indexation appelle __getitem__()
print(playlist[0]) # Output: Song A
print(playlist[1]) # Output: Song B
print(playlist[-1]) # Output: Song C (negative indexing works!)Comme nous déléguons à self.songs[index], toutes les fonctionnalités d’indexation de liste fonctionnent automatiquement : indices positifs, indices négatifs, et même le déclenchement de IndexError pour des indices invalides.
31.8.2) Prendre en charge le slicing avec getitem
La même méthode __getitem__() gère aussi le slicing :
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Get a song by index or slice"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
# Le slicing appelle aussi __getitem__()
print(playlist[1:3]) # Output: ['Song B', 'Song C']
print(playlist[:2]) # Output: ['Song A', 'Song B']
print(playlist[::2]) # Output: ['Song A', 'Song C']Quand vous utilisez le slicing, Python passe un objet slice à __getitem__(). En déléguant à self.songs[index], nous prenons automatiquement en charge toute la syntaxe de slicing.
31.8.3) Implémenter setitem pour l’affectation
La méthode __setitem__() est appelée quand vous assignez à un index :
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
"""Replace a song at a specific index"""
self.songs[index] = value
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(playlist[1]) # Output: Song B
# L’affectation appelle __setitem__()
playlist[1] = "New Song B"
print(playlist[1]) # Output: New Song B31.8.4) Rendre les objets itérables avec getitem
Un effet secondaire intéressant : si vous implémentez __getitem__() avec des indices entiers à partir de 0, votre objet devient automatiquement itérable :
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# Les boucles for fonctionnent automatiquement !
for song in playlist:
print(song)
# Output:
# Song A
# Song B
# Song CPython essaie d’itérer en appelant __getitem__(0), puis __getitem__(1), et ainsi de suite jusqu’à recevoir un IndexError. C’est un ancien protocole d’itération : nous apprendrons le protocole moderne d’itérateur dans le chapitre 35.
31.8.5) Accès de type dictionnaire avec des clés chaînes
__getitem__() et __setitem__() fonctionnent avec n’importe quel type de clé, pas seulement des entiers :
class ScoreBoard:
def __init__(self):
self.scores = {}
def __getitem__(self, player_name):
"""Get score for a player"""
return self.scores.get(player_name, 0)
def __setitem__(self, player_name, score):
"""Set score for a player"""
self.scores[player_name] = score
def __contains__(self, player_name):
return player_name in self.scores
def __len__(self):
return len(self.scores)
scoreboard = ScoreBoard()
# Définir des scores avec des clés chaînes
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
# Mettre à jour un score
scoreboard["Alice"] = 120
# Récupérer des scores
print(scoreboard["Alice"]) # Output: 120
print(scoreboard["Bob"]) # Output: 85
print(scoreboard["Carol"]) # Output: 0
print("Alice" in scoreboard) # Output: True
print(len(scoreboard)) # Output: 2Ce chapitre vous a montré comment créer des classes sophistiquées qui s’intègrent parfaitement à la syntaxe de Python. En implémentant des variables de classe, des propriétés, des méthodes de classe, des méthodes statiques et des méthodes spéciales, vous pouvez faire en sorte que vos classes personnalisées se comportent comme des types intégrés. Dans le chapitre 32, nous explorerons l’héritage et le polymorphisme, qui vous permettent de construire des hiérarchies de classes liées qui partagent et étendent des comportements.