Une introduction – Real Python

By | novembre 27, 2019

trouver un expert Python

Descripteurs Python est une fonctionnalité spécifique qui exploite une grande partie de la magie cachée sous le capot de la langue. Si vous avez déjà pensé que les descripteurs Python constituaient un sujet avancé avec peu d'applications pratiques, alors ce tutoriel est l'outil idéal pour vous aider à comprendre cette fonctionnalité puissante. Vous comprendrez pourquoi les descripteurs Python constituent un sujet aussi intéressant et à quels types de cas d'utilisation vous pouvez les appliquer.

À la fin de ce tutoriel, vous saurez:

  • Quoi Descripteurs Python sont
  • Où ils sont utilisés dans Python internes
  • Comment mettre en place vos propres descripteurs
  • Quand utiliser Descripteurs Python

Ce tutoriel est destiné aux développeurs Python intermédiaires à avancés en ce qui concerne les composants internes à Python. Cependant, si vous n’êtes pas encore à ce niveau, continuez à lire! Vous trouverez des informations utiles sur Python et la chaîne de recherche.

Que sont les descripteurs Python?

Les descripteurs sont des objets Python qui implémentent une méthode du protocole de descripteur, qui vous donne la possibilité de créer des objets ayant un comportement spécial lorsqu’ils sont utilisés comme attributs d’autres objets. Ici vous pouvez voir la définition correcte du protocole de descripteur:

__obtenir__(soi, obj, type=Aucun) -> objet
__ensemble__(soi, obj, valeur) -> Aucun
__supprimer__(soi, obj) -> Aucun
__set_name__(soi, propriétaire, prénom)

Si votre descripteur implémente simplement .__obtenir__(), on dit alors que c’est un descripteur non-data. Si elle implémente .__ensemble__() ou .__supprimer__(), on dit alors que c’est un descripteur de données. Notez que cette différence ne concerne pas seulement le nom, mais aussi le comportement. C’est parce que les descripteurs de données ont la priorité pendant le processus de recherche, comme vous le verrez plus tard.

Jetez un coup d’œil à l’exemple suivant, qui définit un descripteur qui enregistre quelque chose sur la console lors de son accès:

# descriptors.py
classe Verbose_attribute():
    def __obtenir__(soi, obj, type=Aucun) -> objet:
        impression("accéder à l'attribut pour obtenir la valeur")
        revenir 42
    def __ensemble__(soi, obj, valeur) -> Aucun:
        impression("accéder à l'attribut pour définir la valeur")
        élever AttributeError("Impossible de changer la valeur")

classe Foo():
    attribut1 = Verbose_attribute()

my_foo_object = Foo()
X = my_foo_object.attribut1
impression(X)

Dans l'exemple ci-dessus, Verbose_attribute () implémente le protocole de descripteur. Une fois instancié en tant qu'attribut de Foo, cela peut être considéré comme un descripteur.

En tant que descripteur, il a comportement contraignant quand il est accédé en utilisant la notation de point. Dans ce cas, le descripteur enregistre un message sur la console chaque fois qu’on y accède pour obtenir ou définir une valeur:

  • Quand on y accède .__obtenir__() la valeur, elle retourne toujours la valeur 42.
  • Quand on y accède .__ensemble__() une valeur spécifique, il soulève une AttributeError exception, qui est la méthode recommandée pour mettre en œuvre lecture seulement descripteurs.

Maintenant, lancez l’exemple ci-dessus et le descripteur consignera l’accès à la console avant de renvoyer la valeur constante:

$ python descriptors.py
accéder à l'attribut pour obtenir la valeur
42

Ici, quand vous essayez d'accéder attribut1, le descripteur enregistre cet accès à la console, comme défini dans .__obtenir__().

Fonctionnement des descripteurs dans les composants internes de Python

Si vous avez de l'expérience en tant que développeur Python orienté objet, vous pouvez penser que l'approche de l'exemple précédent est un peu exagérée. Vous pouvez obtenir le même résultat en utilisant des propriétés. Bien que cela soit vrai, vous serez peut-être surpris de savoir que les propriétés en Python ne sont que… des descripteurs! Vous verrez plus tard que les propriétés ne sont pas la seule fonctionnalité qui utilise les descripteurs Python.

Descripteurs Python dans les propriétés

Si vous voulez obtenir le même résultat que dans l'exemple précédent sans utiliser explicitement un descripteur Python, l'approche la plus simple consiste à utiliser un propriété. L'exemple suivant utilise une propriété qui enregistre un message dans la console lors de l'accès:

# property_decorator.py
classe Foo():
    @propriété
    def attribut1(soi) -> objet:
        impression("accéder à l'attribut pour obtenir la valeur")
        revenir 42

    @ attribut1.setter
    def attribut1(soi, valeur) -> Aucun:
        impression("accéder à l'attribut pour définir la valeur")
        élever AttributeError("Impossible de changer la valeur")

my_foo_object = Foo()
X = my_foo_object.attribut1
impression(X)

L'exemple ci-dessus utilise les décorateurs pour définir une propriété, mais comme vous le savez peut-être, les décorateurs sont simplement du sucre syntaxique. L’exemple précédent peut en fait s’écrire comme suit:

# property_function.py
classe Foo():
    def getter(soi) -> objet:
        impression("accéder à l'attribut pour obtenir la valeur")
        revenir 42

    def setter(soi, valeur) -> Aucun:
        impression("accéder à l'attribut pour définir la valeur")
        élever AttributeError("Impossible de changer la valeur")

    attribut1 = propriété(getter, setter)

my_foo_object = Foo()
X = my_foo_object.attribut1
impression(X)

Maintenant, vous pouvez voir que la propriété a été créée en utilisant propriété(). La signature de cette fonction est la suivante:

propriété(fget=Aucun, fset=Aucun, Fdel=Aucun, doc=Aucun) -> objet

propriété() renvoie un propriété objet qui implémente le protocole de descripteur. Il utilise les paramètres fget, fset et Fdel pour la mise en œuvre effective des trois méthodes du protocole.

Descripteurs Python dans les méthodes et les fonctions

Si vous avez déjà écrit un programme orienté objet en Python, vous avez certainement utilisé les méthodes. Ce sont des fonctions régulières dont le premier argument est réservé à l'instance d'objet. Lorsque vous accédez à une méthode à l'aide de la notation par points, vous appelez la fonction correspondante et transmettez l'instance de l'objet comme premier paramètre.

La magie qui transforme votre obj.method (* args) Téléphoner à méthode (obj, * args) est à l'intérieur d'un .__obtenir__() implémentation de une fonction objet qui est, en fait, un descripteur non-data. En particulier, le une fonction objet implémente .__obtenir__() de sorte qu'il renvoie une méthode liée lorsque vous y accédez avec la notation par points. le (* args) qui suivent invoquent les fonctions en passant tous les arguments supplémentaires nécessaires.

Pour avoir une idée de son fonctionnement, jetez un oeil à cet exemple pur en Python tiré de la documentation officielle:

classe Une fonction(objet):
    . . .
    def __obtenir__(soi, obj, type d'objet=Aucun):
        "Simuler func_descr_get () dans Objects / funcobject.c"
        si obj est Aucun:
            revenir soi
        revenir les types.MethodType(soi, obj)

Dans l'exemple ci-dessus, lorsque la fonction est utilisée avec la notation par points, .__obtenir__() est appelé et une méthode liée est renvoyée.

Cela fonctionne pour les méthodes d'instance régulière, tout comme pour les méthodes de classe ou les méthodes statiques. Donc, si vous appelez une méthode statique avec obj.method (* args), il est automatiquement transformé en méthode (* args). De même, si vous appelez une méthode de classe avec obj.method (type (obj), * args), il est automatiquement transformé en méthode (type (obj), * args).

Dans la documentation officielle, vous pouvez trouver quelques exemples de la manière dont les méthodes statiques et les méthodes de classe seraient implémentées si elles étaient écrites en Python pur au lieu de l'implémentation C réelle. Par exemple, une implémentation de méthode statique possible pourrait être la suivante:

classe StaticMethod(objet):
    "Émuler PyStaticMethod_Type () dans Objects / funcobject.c"
    def __init__(soi, F):
        soi.F = F

    def __obtenir__(soi, obj, type d'objet=Aucun):
        revenir soi.F

De même, cela pourrait être une implémentation possible de la méthode de classe:

classe ClassMethod(objet):
    "Emulate PyClassMethod_Type () dans Objects / funcobject.c"
    def __init__(soi, F):
        soi.F = F

    def __obtenir__(soi, obj, klass=Aucun):
        si klass est Aucun:
            klass = type(obj)
        def newfunc(*args):
            revenir soi.F(klass, *args)
        revenir newfunc

Notez que, en Python, une méthode de classe est simplement une méthode statique qui prend la référence de classe comme premier argument de la liste d'arguments.

Comment accéder aux attributs avec la chaîne de recherche

Pour en savoir un peu plus sur les descripteurs Python et les éléments internes de Python, vous devez comprendre ce qui se passe dans Python lorsqu'un accès à un attribut est effectué. En Python, chaque objet a une fonction intégrée __dict__ attribut. C'est un dictionnaire qui contient tous les attributs définis dans l'objet même. Pour voir cela en action, considérons l'exemple suivant:

classe Véhicule():
    peut voler = Faux
    nombre_de_weels = 0

classe Voiture(Véhicule):
    nombre_de_weels = 4

    def __init__(soi, Couleur):
        soi.Couleur = Couleur

ma voiture = Voiture("rouge")
impression(ma voiture.__dict__)
impression(type(ma voiture).__dict__)

Ce code crée un nouvel objet et imprime le contenu du fichier. __dict__ attribut pour l'objet et la classe. Maintenant, lancez le script et analysez le résultat pour voir le __dict__ ensemble d'attributs:

'La couleur rouge'
'__module__': '__main__', 'number_of_weels': 4, '__init__': , '__doc__': Aucun

le __dict__ les attributs sont définis comme prévu. Notez qu'en Python, tout est un objet. Une classe est en fait un objet aussi, donc elle aura aussi un __dict__ attribut qui contient tous les attributs et méthodes de la classe.

Alors, que se passe-t-il sous le capot lorsque vous accédez à un attribut en Python? Faisons quelques tests avec une version modifiée de l’exemple précédent. Considérons ce code:

# lookup.py
classe Véhicule(objet):
    peut voler = Faux
    nombre_de_weels = 0

classe Voiture(Véhicule):
    nombre_de_weels = 4

    def __init__(soi, Couleur):
        soi.Couleur = Couleur

ma voiture = Voiture("rouge")

impression(ma voiture.Couleur)
impression(ma voiture.nombre_de_weels)
impression(ma voiture.peut voler)

Dans cet exemple, vous créez une instance du Voiture classe qui hérite de la Véhicule classe. Ensuite, vous accédez à certains attributs. Si vous exécutez cet exemple, vous constaterez que vous obtenez toutes les valeurs que vous attendez:

$ python lookup.py
rouge
4
Faux

Ici, lorsque vous accédez à l'attribut Couleur de l'instance ma voiture, vous accédez en fait à une valeur unique du __dict__ attribut de l'objet ma voiture. Lorsque vous accédez à l'attribut nombre_de_ roues de l'objet ma voiture, vous accédez vraiment à une valeur unique du __dict__ attribut de la classe Voiture. Enfin, lorsque vous accédez à la peut voler attribut, vous y accédez réellement en utilisant le __dict__ attribut de la Véhicule classe.

Cela signifie qu’il est possible de réécrire l’exemple ci-dessus comme ceci:

# lookup2.py
classe Véhicule():
    peut voler = Faux
    nombre_de_weels = 0

classe Voiture(Véhicule):
    nombre_de_weels = 4

    def __init__(soi, Couleur):
        soi.Couleur = Couleur

ma voiture = Voiture("rouge")

impression(ma voiture.__dict__[[[['Couleur'])
impression(type(ma voiture).__dict__[[[['number_of_weels'])
impression(type(ma voiture).__base__.__dict__[[[['peut voler'])

Lorsque vous testez ce nouvel exemple, vous devriez obtenir le même résultat:

$ python lookup2.py
rouge
4
Faux

Alors, que se passe-t-il lorsque vous accédez à l'attribut d'un objet avec la notation par points? Comment l'interprète sait-il ce dont vous avez vraiment besoin? Eh bien, voici où un concept appelé le chaîne de recherche entre:

  • Tout d’abord, vous obtiendrez le résultat renvoyé par le __obtenir__ méthode du descripteur de données nommé d'après l'attribut que vous recherchez.

  • Si cela échoue, vous obtiendrez la valeur de la valeur de votre objet. __dict__ pour la clé nommée d'après l'attribut recherché.

  • Si cela échoue, vous obtiendrez le résultat renvoyé par le __obtenir__ méthode du descripteur non-data nommé d'après l'attribut que vous recherchez.

  • Si cela échoue, vous obtiendrez la valeur de votre type d'objet __dict__ pour la clé nommée d'après l'attribut recherché.

  • Si cela échoue, vous obtiendrez la valeur de votre type d'objet parent __dict__ pour la clé nommée d'après l'attribut recherché.

  • Si cela échoue, l’étape précédente est répétée pour tous les types de parents dans l’ordre de résolution de la méthode de votre objet.

  • Si tout le reste a échoué, alors vous aurez un AttributeError exception.

Maintenant, vous pouvez voir pourquoi il est important de savoir si un descripteur est un Les données descripteur ou un non-données descripteur? Ils se trouvent à différents niveaux de la chaîne de recherche, et vous verrez plus tard que cette différence de comportement peut être très pratique.

Comment utiliser correctement les descripteurs Python

Si vous souhaitez utiliser des descripteurs Python dans votre code, il vous suffit de mettre en œuvre le protocole de descripteur. Les méthodes les plus importantes de ce protocole sont .__obtenir__() et .__ensemble__(), qui ont la signature suivante:

__obtenir__(soi, obj, type=Aucun) -> objet
__ensemble__(soi, obj, valeur) -> Aucun

Lorsque vous implémentez le protocole, gardez ces points à l’esprit:

  • soi est l’instance du descripteur que vous écrivez.
  • obj est l'instance de l'objet auquel votre descripteur est attaché.
  • type est le type d'objet auquel le descripteur est attaché.

Dans .__ensemble__(), vous n'avez pas le type variable, car vous ne pouvez appeler .__ensemble__() sur l'objet. En revanche, vous pouvez appeler .__obtenir__() à la fois l'objet et la classe.

Une autre chose importante à savoir est que les descripteurs Python sont instanciés juste une fois par classe. Cela signifie que chaque instance d'une classe contenant un descripteur actions cette instance de descripteur. C'est quelque chose auquel vous ne vous attendez pas et qui peut conduire à un piège classique, comme ceci:

# descriptors2.py
classe OneDigitNumericValue():
    def __init__(soi):
        soi.valeur = 0
    def __obtenir__(soi, obj, type=Aucun) -> objet:
        revenir soi.valeur
    def __ensemble__(soi, obj, valeur) -> Aucun:
        si valeur > 9 ou valeur < 0 ou int(valeur) ! = valeur:
            élever AttributeError("La valeur n'est pas valide")
        soi.valeur = valeur

classe Foo():
    nombre = OneDigitNumericValue()

my_foo_object = Foo()
mon_second_foo_object = Foo()

my_foo_object.nombre = 3
impression(my_foo_object.nombre)
impression(mon_second_foo_object.nombre)

mon_third_foo_object = Foo()
impression(mon_third_foo_object.nombre)

Ici, vous avez un cours Foo qui définit un attribut nombre, qui est un descripteur. Ce descripteur accepte une valeur numérique à un chiffre et la stocke dans une propriété du descripteur lui-même. Cependant, cette approche ne fonctionnera pas, car chaque instance de Foo partage la même instance de descripteur. Ce que vous avez essentiellement créé n’est qu’un nouvel attribut au niveau de la classe.

Essayez d'exécuter le code et examinez le résultat:

$ python descriptors2.py
3
3
3

Vous pouvez voir que toutes les instances de Foo avoir la même valeur pour l'attribut nombre, même si le dernier a été créé après la my_foo_object.number attribut a été défini.

Alors, comment pouvez-vous résoudre ce problème? Vous pourriez penser que ce serait une bonne idée d’utiliser un dictionnaire pour sauvegarder toutes les valeurs du descripteur pour tous les objets auxquels il est attaché. Cela semble être une bonne solution car .__obtenir__() et .__ensemble__() avoir le obj attribut, qui est l’instance de l’objet auquel vous êtes attaché. Vous pouvez utiliser cette valeur comme clé pour le dictionnaire.

Malheureusement, cette solution présente un inconvénient majeur, que vous pouvez voir dans l'exemple suivant:

# descriptors3.py
classe OneDigitNumericValue():
    def __init__(soi):
        soi.valeur = 

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        essayer:
            revenir soi.valeur[[[[obj]
        sauf:
            revenir 0

    def __ensemble__(soi, obj, valeur) -> Aucun:
        si valeur > 9 ou valeur < 0 ou int(valeur) ! = valeur:
            élever AttributeError("La valeur n'est pas valide")
        soi.valeur[[[[obj] = valeur

classe Foo():
    nombre = OneDigitNumericValue()

my_foo_object = Foo()
mon_second_foo_object = Foo()

my_foo_object.nombre = 3
impression(my_foo_object.nombre)
impression(mon_second_foo_object.nombre)

mon_third_foo_object = Foo()
impression(mon_third_foo_object.nombre)

Dans cet exemple, vous utilisez un dictionnaire pour stocker la valeur du nombre attribut pour tous vos objets dans votre descripteur. Lorsque vous exécutez ce code, vous constaterez qu’il fonctionne correctement et que le comportement est celui attendu:

$ python descriptors3.py
3
0
0

Malheureusement, l’inconvénient est que le descripteur garde une référence forte à l'objet propriétaire. Cela signifie que si vous détruisez l'objet, la mémoire n'est pas libérée car le ramasse-miettes continue de rechercher une référence à cet objet à l'intérieur du descripteur!

Vous pouvez penser que la solution ici pourrait être l’utilisation de références faibles. Cela dit, vous devrez peut-être gérer le fait que tout ne peut pas être considéré comme faible et que, lorsque vos objets sont collectés, ils disparaissent de votre dictionnaire.

La meilleure solution ici est simplement ne pas stocker des valeurs dans le descripteur lui-même, mais de les stocker dans le objet auquel le descripteur est attaché. Essayez cette approche ensuite:

# descriptors4.py
classe OneDigitNumericValue():
    def __init__(soi, prénom):
        soi.prénom = prénom

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        revenir obj.__dict__.obtenir(soi.prénom) ou 0

    def __ensemble__(soi, obj, valeur) -> Aucun:
        obj.__dict__[[[[soi.prénom] = valeur

classe Foo():
    nombre = OneDigitNumericValue("nombre")

my_foo_object = Foo()
mon_second_foo_object = Foo()

my_foo_object.nombre = 3
impression(my_foo_object.nombre)
impression(mon_second_foo_object.nombre)

mon_third_foo_object = Foo()
impression(mon_third_foo_object.nombre)

Dans cet exemple, lorsque vous définissez une valeur sur nombre attribut de votre objet, le descripteur le stocke dans le __dict__ attribut de l’objet auquel il est attaché en utilisant le même nom que le descripteur lui-même.

Le seul problème ici est que lorsque vous instanciez le descripteur, vous devez spécifier le nom en tant que paramètre:

nombre = OneDigitNumericValue("nombre")

Ne vaudrait-il pas mieux écrire nombre = OneDigitNumericValue ()? Mais si vous utilisez une version de Python inférieure à 3.6, vous aurez besoin d’un peu de magie ici avec les métaclasses et les décorateurs. Si vous utilisez Python 3.6 ou supérieur, le protocole de descripteur a une nouvelle méthode .__ nom_ensemble __ () cela fait toute cette magie pour vous, comme proposé dans le PEP 487:

__set_name__(soi, propriétaire, prénom)

Avec cette nouvelle méthode, chaque fois que vous instanciez un descripteur, cette méthode est appelée et le prénom paramètre défini automatiquement.

Maintenant, essayez de réécrire l'ancien exemple de Python 3.6 et versions ultérieures:

# descriptors5.py
classe OneDigitNumericValue():
    def __set_name__(soi, propriétaire, prénom):
        soi.prénom = prénom

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        revenir obj.__dict__.obtenir(soi.prénom) ou 0

    def __ensemble__(soi, obj, valeur) -> Aucun:
        obj.__dict__[[[[soi.prénom] = valeur

classe Foo():
    nombre = OneDigitNumericValue()

my_foo_object = Foo()
mon_second_foo_object = Foo()

my_foo_object.nombre = 3
impression(my_foo_object.nombre)
impression(mon_second_foo_object.nombre)

mon_third_foo_object = Foo()
impression(mon_third_foo_object.nombre)

À présent, .__ init __ () a été enlevé et .__ nom_ensemble __ () a été mis en place. Cela permet de créer votre descripteur sans spécifier le nom de l'attribut interne que vous devez utiliser pour stocker la valeur. Votre code a également l'air plus beau et plus propre maintenant!

Exécutez cet exemple une fois de plus pour vous assurer que tout fonctionne:

$ python descriptors5.py
3
0
0

Cet exemple devrait fonctionner sans problème si vous utilisez Python 3.6 ou supérieur.

Pourquoi utiliser des descripteurs Python?

Maintenant, vous savez ce que sont les descripteurs Python et comment Python les utilise lui-même pour améliorer certaines de ses fonctionnalités, telles que les méthodes et les propriétés. Vous avez également vu comment créer un descripteur Python tout en évitant certains pièges courants. Tout devrait être clair maintenant, mais vous pouvez toujours vous demander pourquoi vous devriez les utiliser.

D'après mon expérience, j'ai rencontré de nombreux développeurs Python avancés qui n'avaient jamais utilisé cette fonctionnalité auparavant et qui n'en avaient pas besoin. C’est tout à fait normal car il n’ya pas beaucoup de cas où les descripteurs Python sont nécessaires. Cependant, cela ne signifie pas que les descripteurs Python ne sont qu'un sujet académique pour les utilisateurs avancés. Il existe encore quelques bons cas d'utilisation qui peuvent justifier le prix à payer pour apprendre à les utiliser.

Propriétés paresseuses

Le premier et le plus simple exemple est propriétés paresseuses. Il s’agit de propriétés dont les valeurs initiales ne sont pas chargées tant qu’ils n’y ont pas accédé pour la première fois. Ensuite, ils chargent leur valeur initiale et la mettent en cache pour la réutiliser ultérieurement.

Prenons l'exemple suivant. Vous avez un cours Pensée profonde qui contient une méthode sens de la vie() qui retourne une valeur après beaucoup de temps passé en forte concentration:

# slow_properties.py
importation au hasard
importation temps

classe Pensée profonde:
    def sens de la vie(soi):
        temps.sommeil(3)
        revenir 42

mon_deep_thought_instance = Pensée profonde()
impression(mon_deep_thought_instance.sens de la vie())
impression(mon_deep_thought_instance.sens de la vie())
impression(mon_deep_thought_instance.sens de la vie())

Si vous exécutez ce code et essayez d'accéder à la méthode trois fois, vous obtenez une réponse toutes les trois secondes, qui correspond à la durée du temps de veille à l'intérieur de la méthode.

Désormais, une propriété paresseuse peut évaluer cette méthode une seule fois lors de sa première exécution. Ensuite, il mettra en cache la valeur obtenue de sorte que, si vous en avez besoin à nouveau, vous puissiez l'obtenir en un rien de temps. Vous pouvez y parvenir en utilisant les descripteurs Python:

# lazy_properties.py
importation au hasard
importation temps

classe LazyProperty:
    def __init__(soi, une fonction):
        soi.une fonction = une fonction
        soi.prénom = une fonction.__prénom__

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        obj.__dict__[[[[soi.prénom] = soi.une fonction(obj)
        revenir obj.__dict__[[[[soi.prénom]

classe Pensée profonde:
    @LazyProperty
    def sens de la vie(soi):
        temps.sommeil(3)
        revenir 42

mon_deep_thought_instance = Pensée profonde()
impression(mon_deep_thought_instance.sens de la vie)
impression(mon_deep_thought_instance.sens de la vie)
impression(mon_deep_thought_instance.sens de la vie)

Prenez votre temps pour étudier ce code et comprendre comment il fonctionne. Pouvez-vous voir le pouvoir des descripteurs Python ici? Dans cet exemple, lorsque vous utilisez le @LazyProperty descripteur, vous instanciez un descripteur et passez-le .sens de la vie(). Ce descripteur stocke la méthode et son nom en tant que variables d'instance.

Comme il s’agit d’un descripteur ne contenant pas de données, lorsque vous accédez pour la première fois à la valeur du paramètre sens de la vie attribut, .__obtenir__() est automatiquement appelé et exécuté .sens de la vie() sur le mon_deep_thought_instance objet. La valeur résultante est stockée dans le __dict__ attribut de l'objet lui-même. Lorsque vous accédez au sens de la vie attribuer à nouveau, Python utilisera le chaîne de recherche pour trouver une valeur pour cet attribut à l'intérieur du __dict__ attribut, et cette valeur sera retournée immédiatement.

Notez que cela fonctionne car, dans cet exemple, vous n’avez utilisé qu’une seule méthode. .__obtenir__() du protocole de descripteur. Vous avez également implémenté un descripteur non-data. Si vous aviez implémenté un descripteur de données, l’astuce n’aurait pas fonctionné. Après la chaîne de recherche, il aurait eu la priorité sur la valeur stockée dans __dict__. Pour tester cela, exécutez le code suivant:

# wrong_lazy_properties.py
importation au hasard
importation temps

classe LazyProperty:
    def __init__(soi, une fonction):
        soi.une fonction = une fonction
        soi.prénom = une fonction.__prénom__

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        obj.__dict__[[[[soi.prénom] = soi.une fonction(obj)
        revenir obj.__dict__[[[[soi.prénom]

    def __ensemble__(soi, obj, valeur):
        passer

classe Pensée profonde:
    @LazyProperty
    def sens de la vie(soi):
        temps.sommeil(3)
        revenir 42

mon_deep_tought_instance = Pensée profonde()
impression(mon_deep_tought_instance.sens de la vie)
impression(mon_deep_tought_instance.sens de la vie)
impression(mon_deep_tought_instance.sens de la vie)

Dans cet exemple, vous pouvez voir que la mise en œuvre .__ensemble__(), même s'il ne fait rien du tout, crée un descripteur de données. Maintenant, l'astuce de la propriété paresseuse cesse de fonctionner.

SEC. Code

Un autre cas d’utilisation typique des descripteurs consiste à écrire du code réutilisable et à créer votre code D.R.Y. Les descripteurs Python offrent aux développeurs un excellent outil pour écrire du code réutilisable pouvant être partagé entre différentes propriétés ou même différentes classes.

Prenons un exemple où vous avez cinq propriétés différentes avec le même comportement. Chaque propriété ne peut être définie sur une valeur spécifique que s’il s’agit d’un nombre pair. Sinon, sa valeur est définie sur 0:

# propriétés.py
classe Valeurs:
    def __init__(soi):
        soi._value1 = 0
        soi._value2 = 0
        soi._value3 = 0
        soi._value4 = 0
        soi._value5 = 0

    @propriété
    def valeur1(soi):
        revenir soi._value1

    @ valeur1.setter
    def valeur1(soi, valeur):
        soi._value1 = valeur si valeur % 2 == 0 autre 0

    @propriété
    def valeur2(soi):
        revenir soi._value2

    @ valeur2.setter
    def valeur2(soi, valeur):
        soi._value2 = valeur si valeur % 2 == 0 autre 0

    @propriété
    def valeur3(soi):
        revenir soi._value3

    @ valeur3.setter
    def valeur3(soi, valeur):
        soi._value3 = valeur si valeur % 2 == 0 autre 0

    @propriété
    def valeur4(soi):
        revenir soi._value4

    @ valeur4.setter
    def valeur4(soi, valeur):
        soi._value4 = valeur si valeur % 2 == 0 autre 0

    @propriété
    def valeur5(soi):
        revenir soi._value5

    @ valeur5.setter
    def valeur5(soi, valeur):
        soi._value5 = valeur si valeur % 2 == 0 autre 0

mes_valeurs = Valeurs()
mes_valeurs.valeur1 = 1
mes_valeurs.valeur2 = 4
impression(mes_valeurs.valeur1)
impression(mes_valeurs.valeur2)

Comme vous pouvez le constater, vous avez beaucoup de code dupliqué ici. Il est possible d’utiliser des descripteurs Python pour partager le comportement entre toutes les propriétés. Vous pouvez créer un Nombre pair descripteur et l'utiliser pour toutes les propriétés comme celle-ci:

# properties2.py
classe Nombre pair:
    def __set_name__(soi, propriétaire, prénom):
        soi.prénom = prénom

    def __obtenir__(soi, obj, type=Aucun) -> objet:
        revenir obj.__dict__.obtenir(soi.prénom) ou 0

    def __ensemble__(soi, obj, valeur) -> Aucun:
        obj.__dict__[[[[soi.prénom] = (valeur si valeur % 2 == 0 autre 0)

classe Valeurs:
    valeur1 = Nombre pair()
    valeur2 = Nombre pair()
    valeur3 = Nombre pair()
    valeur4 = Nombre pair()
    valeur5 = Nombre pair()

mes_valeurs = Valeurs()
mes_valeurs.valeur1 = 1
mes_valeurs.valeur2 = 4
impression(mes_valeurs.valeur1)
impression(mes_valeurs.valeur2)

Ce code semble beaucoup mieux maintenant! Les doublons ont disparu et la logique est maintenant mise en œuvre à un endroit unique. Ainsi, si vous devez la modifier, vous pouvez le faire facilement.

Conclusion

Maintenant que vous savez comment Python utilise les descripteurs pour optimiser certaines de ses superbes fonctionnalités, vous êtes un développeur plus conscient qui comprend pourquoi certaines fonctionnalités de Python ont été implémentées telles quelles.

Vous avez appris:

  • Que sont les descripteurs Python et quand les utiliser?
  • Où les descripteurs sont utilisés dans les internes de Python
  • Comment implémenter vos propres descripteurs

De plus, vous connaissez maintenant des cas d’utilisation spécifiques pour lesquels les descripteurs Python sont particulièrement utiles. Par exemple, les descripteurs sont utiles lorsque vous avez un comportement commun qui doit être partagé entre de nombreuses propriétés, même celles de classes différentes.

Si vous avez des questions, laissez un commentaire en bas ou contactez-moi au Gazouillement! Si vous souhaitez approfondir votre connaissance des descripteurs Python, consultez le Guide pratique des descripteurs Python.