J'essaie de comprendre ce que les descripteurs de Python sont et ce qu'ils sont utiles pour. Je comprends comment ils fonctionnent, mais voici mes doutes. Considérez le code suivant:
class Celsius(object):
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = float(value)
class Temperature(object):
celsius = Celsius()
Pourquoi ai-je besoin de la classe de descripteurs?
Qu'est-ce que c'est
instance
etowner
ici? (en__get__
). Quel est le but de ces paramètres?Comment puis-je appeler / utiliser cet exemple?
la source
self
etinstance
?self
est l'instance de descripteur,instance
est l'instance de la classe (si instanciée) le descripteur est entre (instance.__class__ is owner
).Temperature.celsius
donne la valeur0.0
selon le codecelsius = Celsius()
. Le descripteur Celsius est appelé, donc son instance a la valeur init0.0
attribuée à l'attribut de classe de température, celsius.Il vous donne un contrôle supplémentaire sur le fonctionnement des attributs. Si vous êtes habitué aux getters et setters en Java, par exemple, c'est la façon de faire de Python. Un avantage est qu'il ressemble aux utilisateurs comme un attribut (il n'y a pas de changement de syntaxe). Vous pouvez donc commencer avec un attribut ordinaire, puis, lorsque vous avez besoin de faire quelque chose de fantaisiste, passez à un descripteur.
Un attribut n'est qu'une valeur modifiable. Un descripteur vous permet d'exécuter du code arbitraire lors de la lecture ou de la définition (ou de la suppression) d'une valeur. Vous pouvez donc imaginer l'utiliser pour mapper un attribut à un champ dans une base de données, par exemple - une sorte d'ORM.
Une autre utilisation pourrait être de refuser d'accepter une nouvelle valeur en lançant une exception
__set__
, rendant ainsi "l'attribut" en lecture seule.C'est assez subtil (et la raison pour laquelle j'écris une nouvelle réponse ici - j'ai trouvé cette question en me demandant la même chose et je n'ai pas trouvé la réponse existante si bien).
Un descripteur est défini sur une classe, mais est généralement appelé à partir d'une instance. Quand il est appelé à partir d'une instance à la fois
instance
etowner
est défini (et vous pouvez travailler àowner
partir deinstance
sorte qu'il semble un peu inutile). Mais lorsqu'il est appelé à partir d'une classe, seulowner
est défini - c'est pourquoi il est là.Ceci n'est nécessaire que
__get__
parce que c'est le seul qui peut être appelé sur une classe. Si vous définissez la valeur de classe, vous définissez le descripteur lui-même. De même pour la suppression. C'est pourquoi leowner
n'est pas nécessaire là-bas.Eh bien, voici une astuce intéressante utilisant des classes similaires:
(J'utilise Python 3; pour python 2, vous devez vous assurer que ces divisions sont
/ 5.0
et/ 9.0
). Ça donne:Il existe maintenant d'autres façons, sans doute meilleures, d'obtenir le même effet en python (par exemple, si celsius était une propriété, qui est le même mécanisme de base mais place toute la source à l'intérieur de la classe de température), mais cela montre ce qui peut être fait ...
la source
Les descripteurs sont des attributs de classe (comme des propriétés ou des méthodes) avec l'une des méthodes spéciales suivantes:
__get__
(méthode non descripteur de données, par exemple sur une méthode / fonction)__set__
(méthode de descripteur de données, par exemple sur une instance de propriété)__delete__
(méthode du descripteur de données)Ces objets descripteurs peuvent être utilisés comme attributs sur d'autres définitions de classe d'objets. (Autrement dit, ils vivent dans l'
__dict__
objet de classe.)Les objets descripteurs peuvent être utilisés pour gérer par programme les résultats d'une recherche en pointillés (par exemple
foo.descriptor
) dans une expression normale, une affectation et même une suppression.Fonctions / méthodes, méthodes liées,
property
,classmethod
etstaticmethod
toute utilisation de ces méthodes spéciales pour contrôler la façon dont ils sont accessibles via la recherche en pointillés.Un descripteur de données , comme
property
, peut permettre une évaluation paresseuse des attributs sur la base d'un état plus simple de l'objet, permettant aux instances d'utiliser moins de mémoire que si vous aviez précalculé chaque attribut possible.Un autre descripteur de données, a
member_descriptor
, créé par__slots__
, permet des économies de mémoire en permettant à la classe de stocker des données dans une structure de données de type tuple mutable au lieu de la plus flexible mais consommatrice d'espace__dict__
.Descripteurs non données, généralement exemple, la classe et les méthodes statiques, obtenir leurs premiers arguments implicites (généralement nommés
cls
etself
, respectivement) de leur méthode de descripteur non données,__get__
.La plupart des utilisateurs de Python n'ont besoin d'apprendre que l'utilisation simple et n'ont pas besoin d'apprendre ou de comprendre davantage la mise en œuvre des descripteurs.
En détail: que sont les descripteurs?
Un descripteur est un objet avec l' une des méthodes suivantes (
__get__
,__set__
, ou__delete__
), destinée à être utilisée par-lookup pointillés comme si elle était une caractéristique typique d'une instance. Pour un objet propriétaireobj_instance
, avec undescriptor
objet:obj_instance.descriptor
invoque ledescriptor.__get__(self, obj_instance, owner_class)
retour d'unvalue
C'est ainsi que fonctionnent toutes les méthodes et
get
sur une propriété.obj_instance.descriptor = value
invoque ledescriptor.__set__(self, obj_instance, value)
retourNone
Voici comment fonctionne le
setter
sur une propriété.del obj_instance.descriptor
invoque ledescriptor.__delete__(self, obj_instance)
retourNone
Voici comment fonctionne le
deleter
sur une propriété.obj_instance
est l'instance dont la classe contient l'instance de l'objet descripteur.self
est l'instance du descripteur (probablement un seul pour la classe duobj_instance
)Pour définir cela avec du code, un objet est un descripteur si l'ensemble de ses attributs croise l'un des attributs requis:
Un descripteur de données a un
__set__
et / ou__delete__
.Un non-descripteur de données n'a ni
__set__
ni__delete__
.Exemples d'objets de descripteur intégré:
classmethod
staticmethod
property
Descripteurs non liés aux données
Nous pouvons voir cela
classmethod
etstaticmethod
sommes des non-descripteurs de données:Les deux n'ont que la
__get__
méthode:Notez que toutes les fonctions sont également des non-descripteurs de données:
Descripteur de données,
property
Cependant,
property
est un Data-Descriptor:Ordre de recherche en pointillé
Ce sont des distinctions importantes , car elles affectent l'ordre de recherche pour une recherche en pointillés.
obj_instance
's__dict__
, alorsLa conséquence de cet ordre de recherche est que les non-descripteurs de données comme les fonctions / méthodes peuvent être remplacés par des instances .
Récapitulatif et prochaines étapes
Nous avons appris que les descripteurs sont des objets avec l' une des
__get__
,__set__
ou__delete__
. Ces objets descripteurs peuvent être utilisés comme attributs sur d'autres définitions de classe d'objets. Nous allons maintenant voir comment ils sont utilisés, en utilisant votre code comme exemple.Analyse du code de la question
Voici votre code, suivi de vos questions et réponses à chacun:
Votre descripteur garantit que vous disposez toujours d'un flottant pour cet attribut de classe
Temperature
et que vous ne pouvez pas utiliserdel
pour supprimer l'attribut:Sinon, vos descripteurs ignorent la classe propriétaire et les instances du propriétaire, à la place, stockant l'état dans le descripteur. Vous pouvez tout aussi facilement partager l'état entre toutes les instances avec un simple attribut de classe (tant que vous le définissez toujours comme un flottant pour la classe et que vous ne le supprimez jamais, ou que vous êtes à l'aise avec les utilisateurs de votre code qui le font):
Cela vous donne exactement le même comportement que votre exemple (voir la réponse à la question 3 ci-dessous), mais utilise un Pythons builtin (
property
), et serait considéré comme plus idiomatique:instance
est l'instance du propriétaire qui appelle le descripteur. Le propriétaire est la classe dans laquelle l'objet descripteur est utilisé pour gérer l'accès au point de données. Voir les descriptions des méthodes spéciales qui définissent les descripteurs à côté du premier paragraphe de cette réponse pour des noms de variables plus descriptifs.Voici une démonstration:
Vous ne pouvez pas supprimer l'attribut:
Et vous ne pouvez pas affecter une variable qui ne peut pas être convertie en flottant:
Sinon, ce que vous avez ici est un état global pour toutes les instances, qui est géré en l'attribuant à n'importe quelle instance.
La manière attendue que les programmeurs Python les plus expérimentés atteignent ce résultat serait d'utiliser le
property
décorateur, qui utilise les mêmes descripteurs sous le capot, mais apporte le comportement dans l'implémentation de la classe propriétaire (encore une fois, comme défini ci-dessus):Qui a exactement le même comportement attendu du morceau de code d'origine:
Conclusion
Nous avons couvert les attributs qui définissent les descripteurs, la différence entre les descripteurs de données et les non-descripteurs de données, les objets intégrés qui les utilisent et des questions spécifiques sur l'utilisation.
Encore une fois, comment utiliseriez-vous l'exemple de la question? J'espère que non. J'espère que vous commencerez par ma première suggestion (un attribut de classe simple) et passerez à la deuxième suggestion (le décorateur de propriété) si vous le jugez nécessaire.
la source
Avant d'entrer dans les détails des descripteurs, il peut être important de savoir comment fonctionne la recherche d'attributs en Python. Cela suppose que la classe n'a pas de métaclasse et qu'elle utilise l'implémentation par défaut de
__getattribute__
(les deux peuvent être utilisées pour "personnaliser" le comportement).La meilleure illustration de la recherche d'attributs (dans Python 3.x ou pour les classes de nouveau style dans Python 2.x) dans ce cas est de comprendre les métaclasses Python (le codeelog d'ionel) . L'image utilise
:
comme substitut à la "recherche d'attribut non personnalisable".Cela représente la recherche d'un attribut
foobar
sur l'uninstance
des éléments suivantsClass
:Deux conditions sont ici importantes:
instance
possède une entrée pour le nom d'attribut et qu'elle contient__get__
et__set__
.instance
n'a pas d' entrée pour le nom d'attribut mais que la classe en a un et il en a un__get__
.C'est là que les descripteurs entrent en jeu:
__get__
et__set__
.__get__
.Dans les deux cas, la valeur renvoyée est
__get__
appelée avec l'instance comme premier argument et la classe comme deuxième argument.La recherche est encore plus compliquée pour la recherche d'attribut de classe (voir par exemple Recherche d'attribut de classe (dans le blog mentionné ci-dessus) ).
Passons à vos questions spécifiques:
Dans la plupart des cas, vous n'avez pas besoin d'écrire des classes de descripteurs! Cependant, vous êtes probablement un utilisateur final très régulier. Par exemple des fonctions. Les fonctions sont des descripteurs, c'est ainsi que les fonctions peuvent être utilisées comme des méthodes avec
self
implicitement passé comme premier argument.Si vous recherchez
test_method
une instance, vous obtiendrez une "méthode liée":De même, vous pouvez également lier une fonction en appelant sa
__get__
méthode manuellement (pas vraiment recommandé, juste à des fins d'illustration):Vous pouvez même appeler cette "méthode auto-liée":
Notez que je n'ai fourni aucun argument et que la fonction a renvoyé l'instance que j'avais liée!
Les fonctions sont des descripteurs non-données !
Certains exemples intégrés d'un descripteur de données le seraient
property
. Négligergetter
,setter
etdeleter
leproperty
descripteur est (à partir du guide descriptif HowTo "Propriétés" ):Comme il est un descripteur de données , il est invoqué chaque fois que vous regardez le « nom » du
property
et il délègue simplement aux fonctions décorées avec@property
,@name.setter
et@name.deleter
( le cas échéant).Il y a plusieurs autres descripteurs dans la bibliothèque standard, par exemple
staticmethod
,classmethod
.Le point des descripteurs est facile (bien que vous en ayez rarement besoin): Code commun abstrait pour l'accès aux attributs.
property
est une abstraction pour l'accès aux variables d'instance,function
fournit une abstraction pour les méthodes,staticmethod
fournit une abstraction pour les méthodes qui n'ont pas besoin d'accès aux instances etclassmethod
fournit une abstraction pour les méthodes qui nécessitent un accès aux classes plutôt qu'un accès aux instances (c'est un peu simplifié).Un autre exemple serait une propriété de classe .
Un exemple amusant (à l'aide
__set_name__
de Python 3.6) pourrait également être une propriété qui n'autorise qu'un type spécifique:Ensuite, vous pouvez utiliser le descripteur dans une classe:
Et jouer un peu avec:
Ou une "propriété paresseuse":
Ce sont des cas où le déplacement de la logique dans un descripteur commun pourrait avoir un sens, mais on pourrait également les résoudre (mais peut-être en répétant du code) avec d'autres moyens.
Cela dépend de la façon dont vous recherchez l'attribut. Si vous recherchez l'attribut sur une instance, alors:
Dans le cas où vous recherchez l'attribut sur la classe (en supposant que le descripteur est défini sur la classe):
None
Donc, fondamentalement, le troisième argument est nécessaire si vous souhaitez personnaliser le comportement lorsque vous effectuez une recherche au niveau de la classe (car
instance
c'est le casNone
).Votre exemple est fondamentalement une propriété qui n'autorise que les valeurs qui peuvent être converties
float
et qui sont partagées entre toutes les instances de la classe (et sur la classe - bien que l'on ne puisse utiliser que l'accès en lecture sur la classe, sinon vous remplaceriez l'instance de descripteur ):C'est pourquoi les descripteurs utilisent généralement le deuxième argument (
instance
) pour stocker la valeur et éviter de la partager. Cependant, dans certains cas, le partage d'une valeur entre les instances peut être souhaité (bien que je ne puisse pas penser à un scénario pour le moment). Cependant, cela n'a pratiquement aucun sens pour une propriété Celsius sur une classe de température ... sauf peut-être comme un exercice purement académique.la source
Inspiré par Fluent Python par Buciano Ramalho
Imaginez que vous avez une classe comme celle-ci
Nous devons valider le poids et le prix pour éviter de leur attribuer un nombre négatif, nous pouvons écrire moins de code si nous utilisons le descripteur comme proxy car cela
définissez ensuite la classe LineItem comme ceci:
et nous pouvons étendre la classe Quantité pour faire une validation plus courante
la source
weight = Quantity()
, mais les valeurs doivent être définies dans l'espace de noms d'instance uniquement en utilisantself
(par exempleself.weight = 4
), sinon l'attribut serait renvoyé à la nouvelle valeur et le descripteur serait jeté. Bien!weight = Quantity()
tant que variable de classe et son__get__
et__set__
travaillez sur la variable d'instance. Comment?J'ai essayé (avec des modifications mineures comme suggéré) le code de la réponse d'Andrew Cooke. (J'utilise python 2.7).
Le code:
Le résultat:
Avec Python avant 3, assurez-vous que vous sous-classe de l'objet qui fera fonctionner le descripteur correctement car la magie get ne fonctionne pas pour les anciennes classes de style.
la source
Vous verriez https://docs.python.org/3/howto/descriptor.html#properties
la source