Personnalisation de l'accès aux attributs

PythonPythonBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce laboratoire, vous allez apprendre un aspect fondamental de la programmation orientée objet en Python : l'accès aux attributs. Python permet aux développeurs de personnaliser la manière dont les attributs sont accédés, définis et gérés dans les classes grâce à des méthodes spéciales. Cela offre des moyens puissants de contrôler le comportement des objets.

En outre, vous allez apprendre à personnaliser l'accès aux attributs dans les classes Python, comprendre la différence entre la délégation et l'héritage, et pratiquer la mise en œuvre de la gestion personnalisée des attributs dans les objets Python.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/classes_objects -.-> lab-132502{{"Personnalisation de l'accès aux attributs"}} python/inheritance -.-> lab-132502{{"Personnalisation de l'accès aux attributs"}} python/encapsulation -.-> lab-132502{{"Personnalisation de l'accès aux attributs"}} end

Comprendre __setattr__ pour le contrôle des attributs

En Python, il existe des méthodes spéciales qui vous permettent de personnaliser la manière dont les attributs d'un objet sont accédés et modifiés. Une méthode importante de ce type est __setattr__(). Cette méthode est appelée chaque fois que vous essayez d'assigner une valeur à un attribut d'un objet. Elle vous permet d'avoir un contrôle précis sur le processus d'assignation des attributs.

Qu'est-ce que __setattr__?

La méthode __setattr__(self, name, value) agit comme un intercepteur pour toutes les assignations d'attributs. Lorsque vous écrivez une simple instruction d'assignation comme obj.attr = value, Python ne fait pas simplement une assignation directe de la valeur. Au lieu de cela, il appelle en interne obj.__setattr__("attr", value). Ce mécanisme vous donne le pouvoir de décider ce qui devrait se passer lors de l'assignation de l'attribut.

Voyons maintenant un exemple pratique de l'utilisation de __setattr__ pour restreindre quels attributs peuvent être définis sur une classe.

Étape 1 : Créer un nouveau fichier

Tout d'abord, ouvrez un nouveau fichier dans l'IDE Web. Vous pouvez le faire en cliquant sur le menu "File" puis en sélectionnant "New File". Nommez ce fichier restricted_stock.py et enregistrez - le dans le répertoire /home/labex/project. Ce fichier contiendra la définition de la classe où nous utiliserons __setattr__ pour contrôler l'assignation des attributs.

Étape 2 : Ajouter du code à restricted_stock.py

Ajoutez le code suivant au fichier restricted_stock.py. Ce code définit une classe RestrictedStock.

class RestrictedStock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def __setattr__(self, name, value):
        ## Only allow specific attributes
        if name not in {'name', 'shares', 'price'}:
            raise AttributeError(f'Cannot set attribute {name}')

        ## If attribute is allowed, set it using the parent method
        super().__setattr__(name, value)

Dans la méthode __init__, nous initialisons l'objet avec les attributs name, shares et price. La méthode __setattr__ vérifie si le nom de l'attribut en cours d'assignation se trouve dans l'ensemble des attributs autorisés (name, shares, price). Si ce n'est pas le cas, elle lève une AttributeError. Si l'attribut est autorisé, elle utilise la méthode __setattr__ de la classe parente pour définir effectivement l'attribut.

Étape 3 : Créer un fichier de test

Créez un nouveau fichier appelé test_restricted.py et ajoutez le code suivant. Ce code testera la fonctionnalité de la classe RestrictedStock.

from restricted_stock import RestrictedStock

## Create a new stock
stock = RestrictedStock('GOOG', 100, 490.1)

## Test accessing existing attributes
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")

## Test modifying an existing attribute
print("\nChanging shares to 75...")
stock.shares = 75
print(f"New shares value: {stock.shares}")

## Test setting an invalid attribute
try:
    print("\nTrying to set an invalid attribute 'share'...")
    stock.share = 50
except AttributeError as e:
    print(f"Error: {e}")

Dans ce code, nous importons d'abord la classe RestrictedStock. Ensuite, nous créons une instance de la classe. Nous testons l'accès aux attributs existants, la modification d'un attribut existant et, enfin, nous essayons de définir un attribut invalide pour voir si la méthode __setattr__ fonctionne comme prévu.

Étape 4 : Exécuter le fichier de test

Ouvrez un terminal dans l'IDE Web et exécutez les commandes suivantes pour exécuter le fichier test_restricted.py :

cd /home/labex/project
python3 test_restricted.py

Après avoir exécuté ces commandes, vous devriez voir une sortie similaire à celle - ci :

Name: GOOG
Shares: 100
Price: 490.1

Changing shares to 75...
New shares value: 75

Trying to set an invalid attribute 'share'...
Error: Cannot set attribute share

Comment cela fonctionne

La méthode __setattr__ dans notre classe RestrictedStock fonctionne selon les étapes suivantes :

  1. Elle vérifie d'abord si le nom de l'attribut se trouve dans l'ensemble autorisé (name, shares, price).
  2. Si le nom de l'attribut n'est pas dans l'ensemble autorisé, elle lève une AttributeError. Cela empêche l'assignation d'attributs indésirables.
  3. Si l'attribut est autorisé, elle utilise super().__setattr__() pour définir effectivement l'attribut. Cela garantit que le processus normal d'assignation d'attributs a lieu pour les attributs autorisés.

Cette méthode est plus flexible que l'utilisation de __slots__, que nous avons vu dans des exemples précédents. Bien que __slots__ puisse optimiser l'utilisation de la mémoire et restreindre les attributs, il a des limitations lorsqu'il s'agit de l'héritage et peut entrer en conflit avec d'autres fonctionnalités de Python. Notre approche avec __setattr__ nous donne un contrôle similaire sur l'assignation des attributs sans certaines de ces limitations.

Création d'objets en lecture seule avec des proxies

Dans cette étape, nous allons explorer les classes proxy, un modèle très utile en Python. Les classes proxy vous permettent de prendre un objet existant et de modifier son comportement sans altérer son code d'origine. C'est comme mettre une enveloppe spéciale autour d'un objet pour ajouter de nouvelles fonctionnalités ou des restrictions.

Qu'est-ce qu'un proxy ?

Un proxy est un objet qui se place entre vous et un autre objet. Il a le même ensemble de fonctions et de propriétés que l'objet d'origine, mais il peut faire des choses supplémentaires. Par exemple, il peut contrôler qui peut accéder à l'objet, conserver un enregistrement des actions (journalisation) ou ajouter d'autres fonctionnalités utiles.

Créons un proxy en lecture seule. Ce type de proxy vous empêchera de modifier les attributs d'un objet.

Étape 1 : Créer la classe de proxy en lecture seule

Tout d'abord, nous devons créer un fichier Python qui définit notre proxy en lecture seule.

  1. Accédez au répertoire /home/labex/project.
  2. Créez un nouveau fichier nommé readonly_proxy.py dans ce répertoire.
  3. Ouvrez le fichier readonly_proxy.py et ajoutez le code suivant :
class ReadonlyProxy:
    def __init__(self, obj):
        ## Store the wrapped object directly in __dict__ to avoid triggering __setattr__
        self.__dict__['_obj'] = obj

    def __getattr__(self, name):
        ## Forward attribute access to the wrapped object
        return getattr(self._obj, name)

    def __setattr__(self, name, value):
        ## Block all attribute assignments
        raise AttributeError("Cannot modify a read-only object")

Dans ce code, la classe ReadonlyProxy est définie. La méthode __init__ stocke l'objet que nous voulons envelopper. Nous utilisons self.__dict__ pour le stocker directement afin d'éviter d'appeler la méthode __setattr__. La méthode __getattr__ est utilisée lorsque nous essayons d'accéder à un attribut du proxy. Elle transmet simplement la demande à l'objet enveloppé. La méthode __setattr__ est appelée lorsque nous essayons de modifier un attribut. Elle lève une erreur pour empêcher toute modification.

Étape 2 : Créer un fichier de test

Maintenant, nous allons créer un fichier de test pour voir comment fonctionne notre proxy en lecture seule.

  1. Créez un nouveau fichier nommé test_readonly.py dans le même répertoire /home/labex/project.
  2. Ajoutez le code suivant au fichier test_readonly.py :
from stock import Stock
from readonly_proxy import ReadonlyProxy

## Create a normal Stock object
stock = Stock('AAPL', 100, 150.75)
print("Original stock object:")
print(f"Name: {stock.name}")
print(f"Shares: {stock.shares}")
print(f"Price: {stock.price}")
print(f"Cost: {stock.cost}")

## Modify the original stock object
stock.shares = 200
print(f"\nAfter modification - Shares: {stock.shares}")
print(f"After modification - Cost: {stock.cost}")

## Create a read-only proxy around the stock
readonly_stock = ReadonlyProxy(stock)
print("\nRead-only proxy object:")
print(f"Name: {readonly_stock.name}")
print(f"Shares: {readonly_stock.shares}")
print(f"Price: {readonly_stock.price}")
print(f"Cost: {readonly_stock.cost}")

## Try to modify the read-only proxy
try:
    print("\nAttempting to modify the read-only proxy...")
    readonly_stock.shares = 300
except AttributeError as e:
    print(f"Error: {e}")

## Show that the original object is unchanged
print(f"\nOriginal stock shares are still: {stock.shares}")

Dans ce code de test, nous créons d'abord un objet Stock normal et affichons ses informations. Ensuite, nous modifions l'un de ses attributs et affichons les informations mises à jour. Ensuite, nous créons un proxy en lecture seule pour l'objet Stock et affichons ses informations. Enfin, nous essayons de modifier le proxy en lecture seule et nous nous attendons à recevoir une erreur.

Étape 3 : Exécuter le script de test

Après avoir créé la classe de proxy et le fichier de test, nous devons exécuter le script de test pour voir les résultats.

  1. Ouvrez un terminal et accédez au répertoire /home/labex/project en utilisant la commande suivante :
cd /home/labex/project
  1. Exécutez le script de test en utilisant la commande suivante :
python3 test_readonly.py

Vous devriez voir une sortie similaire à :

Original stock object:
Name: AAPL
Shares: 100
Price: 150.75
Cost: 15075.0

After modification - Shares: 200
After modification - Cost: 30150.0

Read-only proxy object:
Name: AAPL
Shares: 200
Price: 150.75
Cost: 30150.0

Attempting to modify the read-only proxy...
Error: Cannot modify a read-only object

Original stock shares are still: 200

Comment le proxy fonctionne

La classe ReadonlyProxy utilise deux méthodes spéciales pour obtenir sa fonctionnalité en lecture seule :

  1. __getattr__(self, name) : Cette méthode est appelée lorsque Python ne peut pas trouver un attribut de la manière normale. Dans notre classe ReadonlyProxy, nous utilisons la fonction getattr() pour transférer la demande d'accès à l'attribut à l'objet enveloppé. Donc, lorsque vous essayez d'accéder à un attribut du proxy, il obtiendra en fait l'attribut de l'objet enveloppé.

  2. __setattr__(self, name, value) : Cette méthode est appelée lorsque vous essayez d'assigner une valeur à un attribut. Dans notre implémentation, nous levons une AttributeError pour empêcher toute modification des attributs du proxy.

  3. Dans la méthode __init__, nous modifions directement self.__dict__ pour stocker l'objet enveloppé. Cela est important car si nous utilisions la manière normale d'assigner l'objet, cela appellerait la méthode __setattr__, qui lèverait une erreur.

Ce modèle de proxy nous permet d'ajouter une couche en lecture seule autour de n'importe quel objet existant sans modifier sa classe d'origine. L'objet proxy se comporte comme l'objet enveloppé, mais ne vous laissera pas effectuer de modifications.

La délégation comme alternative à l'héritage

En programmation orientée objet, la réutilisation et l'extension de code sont des tâches courantes. Il existe deux principales façons d'y parvenir : l'héritage et la délégation.

L'héritage est un mécanisme par lequel une sous - classe hérite des méthodes et des attributs d'une classe mère. La sous - classe peut choisir de remplacer certaines de ces méthodes héritées pour fournir sa propre implémentation.

La délégation, en revanche, consiste à ce qu'un objet contienne un autre objet et transmette des appels de méthode spécifiques à celui - ci.

Dans cette étape, nous allons explorer la délégation comme alternative à l'héritage. Nous allons implémenter une classe qui délègue une partie de son comportement à un autre objet.

Configuration d'un exemple de délégation

Tout d'abord, nous devons configurer la classe de base avec laquelle notre classe délégatrice va interagir.

  1. Créez un nouveau fichier appelé base_class.py dans le répertoire /home/labex/project. Ce fichier définira une classe nommée Spam avec trois méthodes : method_a, method_b et method_c. Chaque méthode affiche un message et retourne un résultat. Voici le code à placer dans base_class.py :
class Spam:
    def method_a(self):
        print('Spam.method_a executed')
        return "Result from Spam.method_a"

    def method_b(self):
        print('Spam.method_b executed')
        return "Result from Spam.method_b"

    def method_c(self):
        print('Spam.method_c executed')
        return "Result from Spam.method_c"

Ensuite, nous allons créer la classe délégatrice.

  1. Créez un nouveau fichier appelé delegator.py. Dans ce fichier, nous allons définir une classe nommée DelegatingSpam qui délègue une partie de son comportement à une instance de la classe Spam.
from base_class import Spam

class DelegatingSpam:
    def __init__(self):
        ## Create an instance of Spam that we'll delegate to
        self._spam = Spam()

    def method_a(self):
        ## Override method_a but also call the original
        print('DelegatingSpam.method_a executed')
        result = self._spam.method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('DelegatingSpam.method_c executed')
        return "Result from DelegatingSpam.method_c"

    def __getattr__(self, name):
        ## For any other attribute/method, delegate to self._spam
        print(f"Delegating {name} to the wrapped Spam object")
        return getattr(self._spam, name)

Dans la méthode __init__, nous créons une instance de la classe Spam. La méthode method_a remplace la méthode d'origine mais appelle également la méthode method_a de la classe Spam. La méthode method_c remplace complètement la méthode d'origine. La méthode __getattr__ est une méthode spéciale en Python qui est appelée lorsqu'un attribut ou une méthode qui n'existe pas dans la classe DelegatingSpam est accédé. Elle transmet alors l'appel à l'instance de Spam.

Maintenant, créons un fichier de test pour vérifier notre implémentation.

  1. Créez un fichier de test nommé test_delegation.py. Ce fichier créera une instance de la classe DelegatingSpam et appellera ses méthodes.
from delegator import DelegatingSpam

## Create a delegating object
spam = DelegatingSpam()

print("Calling method_a (overridden with delegation):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (not defined in DelegatingSpam, delegated):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Enfin, nous allons exécuter le script de test.

  1. Exécutez le script de test en utilisant les commandes suivantes dans le terminal :
cd /home/labex/project
python3 test_delegation.py

Vous devriez voir une sortie similaire à ce qui suit :

Calling method_a (overridden with delegation):
DelegatingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (not defined in DelegatingSpam, delegated):
Delegating method_b to the wrapped Spam object
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
DelegatingSpam.method_c executed
Result: Result from DelegatingSpam.method_c

Calling non-existent method_d:
Delegating method_d to the wrapped Spam object
Error: 'Spam' object has no attribute 'method_d'

Délégation vs. Héritage

Maintenant, comparons la délégation avec l'héritage traditionnel.

  1. Créez un fichier appelé inheritance_example.py. Dans ce fichier, nous allons définir une classe nommée InheritingSpam qui hérite de la classe Spam.
from base_class import Spam

class InheritingSpam(Spam):
    def method_a(self):
        ## Override method_a but also call the parent method
        print('InheritingSpam.method_a executed')
        result = super().method_a()
        return f"Modified {result}"

    def method_c(self):
        ## Completely override method_c
        print('InheritingSpam.method_c executed')
        return "Result from InheritingSpam.method_c"

La classe InheritingSpam remplace les méthodes method_a et method_c. Dans la méthode method_a, nous utilisons super() pour appeler la méthode method_a de la classe mère.

Ensuite, nous allons créer un fichier de test pour l'exemple d'héritage.

  1. Créez un fichier de test nommé test_inheritance.py. Ce fichier créera une instance de la classe InheritingSpam et appellera ses méthodes.
from inheritance_example import InheritingSpam

## Create an inheriting object
spam = InheritingSpam()

print("Calling method_a (overridden with super call):")
result_a = spam.method_a()
print(f"Result: {result_a}\n")

print("Calling method_b (inherited from parent):")
result_b = spam.method_b()
print(f"Result: {result_b}\n")

print("Calling method_c (completely overridden):")
result_c = spam.method_c()
print(f"Result: {result_c}\n")

## Try accessing a non-existent method
try:
    print("Calling non-existent method_d:")
    spam.method_d()
except AttributeError as e:
    print(f"Error: {e}")

Enfin, nous allons exécuter le test d'héritage.

  1. Exécutez le test d'héritage en utilisant les commandes suivantes dans le terminal :
cd /home/labex/project
python3 test_inheritance.py

Vous devriez voir une sortie similaire à ce qui suit :

Calling method_a (overridden with super call):
InheritingSpam.method_a executed
Spam.method_a executed
Result: Modified Result from Spam.method_a

Calling method_b (inherited from parent):
Spam.method_b executed
Result: Result from Spam.method_b

Calling method_c (completely overridden):
InheritingSpam.method_c executed
Result: Result from InheritingSpam.method_c

Calling non-existent method_d:
Error: 'InheritingSpam' object has no attribute 'method_d'

Principales différences et considérations

Regardons les similitudes et les différences entre la délégation et l'héritage.

  1. Remplacement de méthode : La délégation et l'héritage vous permettent de remplacer des méthodes, mais la syntaxe est différente.

    • En délégation, vous définissez votre propre méthode et décidez si vous appelez la méthode de l'objet enveloppé.
    • En héritage, vous définissez votre propre méthode et utilisez super() pour appeler la méthode de la classe mère.
  2. Accès aux méthodes :

    • En délégation, les méthodes non définies sont transmises via la méthode __getattr__.
    • En héritage, les méthodes non définies sont héritées automatiquement.
  3. Relations de type :

    • Avec la délégation, isinstance(delegating_spam, Spam) retourne False car l'objet DelegatingSpam n'est pas une instance de la classe Spam.
    • Avec l'héritage, isinstance(inheriting_spam, Spam) retourne True car la classe InheritingSpam hérite de la classe Spam.
  4. Limitations : La délégation via __getattr__ ne fonctionne pas avec les méthodes spéciales comme __getitem__, __len__, etc. Ces méthodes devraient être explicitement définies dans la classe délégatrice.

La délégation est particulièrement utile dans les situations suivantes :

  • Vous souhaitez personnaliser le comportement d'un objet sans affecter sa hiérarchie.
  • Vous souhaitez combiner des comportements de plusieurs objets qui n'ont pas de parent commun.
  • Vous avez besoin de plus de flexibilité que ce que l'héritage offre.

L'héritage est généralement préféré lorsque :

  • La relation "est - un" est claire (par exemple, une Voiture est un Véhicule).
  • Vous devez maintenir la compatibilité de type dans votre code.
  • Les méthodes spéciales doivent être héritées.

Résumé

Dans ce laboratoire, vous avez appris à connaître des mécanismes puissants de Python pour personnaliser l'accès aux attributs et le comportement des objets. Vous avez exploré comment utiliser __setattr__ pour contrôler quels attributs peuvent être définis sur un objet, permettant ainsi un accès contrôlé aux propriétés des objets. De plus, vous avez implémenté un proxy en lecture seule pour envelopper des objets existants, empêchant les modifications tout en préservant leur fonctionnalité.

Vous avez également approfondi la différence entre la délégation et l'héritage pour la réutilisation et la personnalisation du code. En utilisant __getattr__, vous avez appris à transférer les appels de méthode à un objet enveloppé. Ces techniques offrent des moyens flexibles de contrôler le comportement des objets au - delà de l'héritage standard, utiles pour créer des interfaces contrôlées, implémenter des restrictions d'accès, ajouter des comportements transversaux et composer des comportements à partir de plusieurs sources. Comprendre ces modèles vous aide à écrire un code Python plus maintenable et flexible.