Explorer les Méthodes Spéciales dans les Classes Python

PythonBeginner
Pratiquer maintenant

Introduction

Dans ce laboratoire, vous explorerez certaines des méthodes spéciales de Python, souvent appelées méthodes "dunder" en raison de leurs noms à double trait de soulignement. Vous acquerrez une compréhension pratique de la manière dont ces méthodes vous permettent de personnaliser le comportement de vos classes et de vos objets.

Vous apprendrez à connaître la méthode __new__ pour contrôler la création d'instances et la méthode __del__ pour la destruction d'objets. Vous verrez également comment utiliser __slots__ pour optimiser l'utilisation de la mémoire et restreindre les attributs, et comment rendre vos instances de classe appelables comme des fonctions grâce à la méthode __call__. Grâce à des exemples pratiques, vous apprendrez à écrire un code Python plus efficace et plus expressif.

Ceci est un Guided Lab, qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent que c'est un laboratoire de niveau débutant avec un taux de réussite de 100%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

Comprendre et Utiliser la Méthode __new__

Dans cette étape, vous allez explorer la méthode __new__. Alors que __init__ est couramment utilisée pour initialiser les attributs d'un objet après sa création, __new__ est la méthode qui crée réellement l'instance en premier lieu. Elle est appelée avant __init__.

Voici les différences clés :

  • __new__ est une méthode statique qui prend la classe (cls) comme premier argument. Elle est responsable de la création et du retour d'une nouvelle instance de la classe.
  • __init__ est une méthode d'instance qui prend l'instance (self) comme premier argument. Elle initialise l'objet nouvellement créé et ne retourne rien.

Vous n'avez généralement pas besoin de surcharger __new__ car l'implémentation par défaut de la classe object est suffisante. Cependant, elle est utile pour les cas avancés comme l'implémentation du patron de conception Singleton ou la création d'instances de types immuables.

Voyons __new__ en action. Vous allez créer une classe Dog qui affiche un message lors de la création de l'instance.

Tout d'abord, ouvrez le fichier dog_cat.py dans l'explorateur de fichiers situé sur le côté gauche de l'IDE.

Ajoutez le code suivant dans le fichier dog_cat.py. Ce code définit une classe de base Animal et une sous-classe Dog qui surcharge la méthode __new__.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name
        print(f'Initializing {self._name} in Animal.')

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    ## Le premier paramètre est cls, qui fait référence à la classe elle-même.
    ## Il doit également accepter tous les arguments passés au constructeur.
    def __new__(cls, name, age):
        print('A new Dog instance is being created.')
        ## Appeler la méthode __new__ de la classe parente pour créer l'instance.
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        print(f'Initializing {name} in Dog.')
        ## Le __init__ de la classe parente est appelé pour définir le nom.
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

## Create a Dog instance
print("Creating a Dog object...")
d = Dog('Buddy', 5)
print("Dog object created.")
print(f"Dog's name: {d._name}, Age: {d.age}")

Enregistrez le fichier (Ctrl+S ou Cmd+S).

Maintenant, ouvrez un terminal dans votre IDE (vous pouvez utiliser le menu Terminal > New Terminal). Exécutez le script pour observer l'ordre des appels de méthodes.

python ~/project/dog_cat.py

Vous verrez la sortie suivante. Remarquez que __new__ est appelée en premier pour créer l'instance, suivie des méthodes __init__ pour l'initialiser.

Creating a Dog object...
A new Dog instance is being created.
Initializing Buddy in Dog.
Initializing Buddy in Animal.
Dog object created.
Dog's name: Buddy, Age: 5

Ceci démontre que __new__ contrôle la création de l'objet, et __init__ le configure par la suite.

Implémenter et Tester la Méthode __del__

Dans cette étape, vous allez découvrir la méthode __del__. Cette méthode est appelée un finaliseur ou un destructeur. Elle est invoquée lorsque le compte de références d'un objet tombe à zéro, ce qui signifie qu'il est sur le point d'être détruit par le ramasse-miettes (garbage collector) de Python. Elle est souvent utilisée pour des tâches de nettoyage, comme la fermeture de connexions réseau ou de descripteurs de fichiers (file handles).

Vous pouvez supprimer une référence à un objet en utilisant l'instruction del. Lorsque la dernière référence disparaît, __del__ est automatiquement appelée.

Ajoutons une méthode __del__ à notre classe Dog pour voir quand un objet est détruit.

Ouvrez à nouveau le fichier dog_cat.py. Remplacez l'intégralité du contenu du fichier par le code suivant. Cette version supprime le code qui crée une instance de Dog (pour éviter de la créer lors de l'importation du module), et elle ajoute la méthode __del__ à la classe Dog.

## File Name: dog_cat.py

class Animal:
    def __init__(self, name):
        self._name = name

    def say(self):
        print(self._name + ' is saying something')

class Dog(Animal):
    def __new__(cls, name, age):
        instance = super().__new__(cls)
        return instance

    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    def say(self):
        print(self._name + ' is making a sound: wang wang wang...')

    ## Add this method to the Dog class
    def __del__(self):
        print(f'The Dog object {self._name} is being deleted.')

Enregistrez le fichier dog_cat.py.

Maintenant, créons un script séparé pour tester ce comportement. Ouvrez le fichier test_del.py dans l'explorateur de fichiers.

Ajoutez le code suivant à test_del.py. Ce script créera deux instances de Dog puis en supprimera explicitement une.

## File Name: test_del.py

from dog_cat import Dog
import time

print("Creating two Dog objects: d1 and d2.")
d1 = Dog('Tom', 3)
d2 = Dog('John', 5)

print("\nDeleting reference to d1...")
del d1
print("Reference to d1 deleted.")

## The garbage collector may not run immediately.
## We add a small delay to give it time to run.
time.sleep(1)

print("\nScript is about to end. d2 will be deleted automatically.")

Enregistrez le fichier. Maintenant, exécutez le script test_del.py dans le terminal.

python ~/project/test_del.py

Observez la sortie. Le message __del__ pour Tom apparaît après l'appel de del d1. Le message pour John apparaît à la toute fin, car l'objet d2 est collecté par le ramasse-miettes lorsque le script se termine.

Creating two Dog objects: d1 and d2.

Deleting reference to d1...
The Dog object Tom is being deleted.
Reference to d1 deleted.

Script is about to end. d2 will be deleted automatically.
The Dog object John is being deleted.

Note: Le moment exact de la collecte des déchets peut varier. __del__ est appelée lorsque l'objet est collecté, et pas nécessairement immédiatement après l'utilisation de del.

Contrôler les Attributs avec __slots__

Dans cette étape, vous allez découvrir __slots__. Par défaut, Python stocke les attributs d'instance dans un dictionnaire spécial appelé __dict__. Cela vous permet d'ajouter de nouveaux attributs à un objet à tout moment. Cependant, cette flexibilité consomme de la mémoire supplémentaire.

En définissant un attribut __slots__ dans votre classe, vous pouvez spécifier une liste fixe d'attributs que les instances peuvent posséder. Cela a deux effets principaux :

  1. Économie de Mémoire : Python utilise une structure interne plus compacte au lieu d'un __dict__ pour chaque instance, ce qui peut réduire considérablement l'utilisation de la mémoire, surtout lors de la création de nombreux objets.
  2. Restriction d'Attributs : Vous ne pouvez plus ajouter à une instance des attributs qui ne sont pas listés dans __slots__. Cela aide à prévenir les fautes de frappe et à imposer une structure d'objet stricte.

Créons un exemple pour voir comment fonctionne __slots__. Ouvrez le fichier slots_example.py dans l'explorateur de fichiers.

Ajoutez le code suivant à slots_example.py :

## File Name: slots_example.py

class Player:
    ## Define the allowed attributes using __slots__
    __slots__ = ('name', 'level')

    def __init__(self, name, level):
        self.name = name
        self.level = level

## Create an instance of Player
p1 = Player('Hero', 10)

## Access the allowed attributes
print(f"Player name: {p1.name}")
print(f"Player level: {p1.level}")

## Now, try to add a new attribute that is NOT in __slots__
print("\nTrying to add a 'score' attribute...")
try:
    p1.score = 100
    print(f"Player score: {p1.score}")
except AttributeError as e:
    print(f"Caught an error: {e}")

## Also, check if the instance has a __dict__ attribute
print("\nChecking for __dict__...")
try:
    print(p1.__dict__)
except AttributeError as e:
    print(f"Caught an error: {e}")

Enregistrez le fichier. Maintenant, exécutez le script slots_example.py dans le terminal.

python ~/project/slots_example.py

La sortie montre que vous pouvez assigner des valeurs à name et level, mais tenter d'assigner une valeur à score lève une AttributeError. Cela confirme également que l'instance ne possède pas d'attribut __dict__.

Player name: Hero
Player level: 10

Trying to add a 'score' attribute...
Caught an error: 'Player' object has no attribute 'score'

Checking for __dict__...
Caught an error: 'Player' object has no attribute '__dict__'

Ceci démontre comment __slots__ peut imposer un ensemble fixe d'attributs et optimiser la mémoire en éliminant le dictionnaire d'instance.

Rendre les Instances Appelables avec __call__

Dans cette étape, vous allez explorer la méthode __call__. En Python, les objets qui peuvent être "appelés" en utilisant des parenthèses () comme des fonctions sont connus sous le nom d'objets appelables (callable objects). Les fonctions et les méthodes sont naturellement appelables.

Par défaut, les instances de classe ne sont pas appelables. Cependant, si vous définissez la méthode spéciale __call__ dans une classe, ses instances deviennent appelables. Lorsque vous appelez une instance comme une fonction, le code à l'intérieur de sa méthode __call__ est exécuté. Ceci est utile pour créer des objets qui se comportent comme des fonctions mais peuvent également maintenir leur propre état interne.

Créons une classe dont les instances peuvent être appelées. Ouvrez le fichier callable_instance.py dans l'explorateur de fichiers.

Ajoutez le code suivant à callable_instance.py :

## File Name: callable_instance.py

class Greeter:
    def __init__(self, greeting):
        ## This state is stored with the instance
        self.greeting = greeting
        print(f'Greeter initialized with "{self.greeting}"')

    ## Define the __call__ method to make instances callable
    def __call__(self, name):
        ## This code runs when the instance is called
        print(f"{self.greeting}, {name}!")

## Create an instance of Greeter
hello_greeter = Greeter("Hello")

## Check if the instance is callable using the built-in callable() function
print(f"Is hello_greeter callable? {callable(hello_greeter)}")

## Now, call the instance as if it were a function
print("\nCalling the instance:")
hello_greeter("Alice")
hello_greeter("Bob")

## Create another instance with a different state
goodbye_greeter = Greeter("Goodbye")
print("\nCalling the second instance:")
goodbye_greeter("Charlie")

Enregistrez le fichier. Maintenant, exécutez le script callable_instance.py dans le terminal.

python ~/project/callable_instance.py

La sortie montre que l'instance hello_greeter est bien appelable. Chaque fois que vous l'appelez, la méthode __call__ est exécutée, en utilisant l'état (self.greeting) qui a été défini lors de l'initialisation.

Greeter initialized with "Hello"
Is hello_greeter callable? True

Calling the instance:
Hello, Alice!
Hello, Bob!
Greeter initialized with "Goodbye"

Calling the second instance:
Goodbye, Charlie!

Ceci démontre comment __call__ vous permet de créer des objets d'état (stateful) qui ressemblent à des fonctions, ce qui est une fonctionnalité puissante en programmation orientée objet.

Résumé

Dans ce laboratoire, vous avez exploré plusieurs méthodes spéciales puissantes en Python. Vous avez appris à utiliser __new__ pour contrôler le processus de création d'instance, vous donnant un point d'accroche avant que __init__ ne soit appelé. Vous avez implémenté la méthode __del__ pour définir une logique de nettoyage qui s'exécute lorsque l'objet est collecté par le ramasse-miettes (garbage collected). Vous avez également utilisé __slots__ pour optimiser la mémoire et imposer un modèle d'attribut strict en empêchant la création d'un __dict__ d'instance. Enfin, vous avez fait en sorte que vos objets se comportent comme des fonctions en implémentant la méthode __call__. En maîtrisant ces méthodes spéciales (dunder methods), vous pouvez écrire des classes plus flexibles, efficaces et Pythoniques.