Cet article est une compilation du contenu annoncé à PyCon JP 2014 qui s'est tenue du 12 au 14 septembre 2014.
Un descripteur est un objet qui définit les méthodes suivantes.
class Descriptor(object):
def __get__(self, obj, type=None): pass
def __set__(self, obj, value): pass
def __delete__(self, obj): pass
En Python, un ensemble de méthodes qu'un objet avec une propriété spécifique devrait implémenter est appelé protocole (un protocole typique est le protocole Iterator (http://docs.python.jp/3.4/library/stdtypes.). html # typeiter) etc.). Les descripteurs sont l'un de ces protocoles.
Ce descripteur est utilisé derrière les fonctionnalités de base de Python telles que les propriétés, les méthodes (méthodes statiques, méthodes de classe, méthodes d'instance) et super
. Les descripteurs sont également un protocole générique et peuvent être définis par l'utilisateur.
Il existe deux principaux types de descripteurs.
__get__ '' et
set ''Les descripteurs de données se comportent comme un accès normal aux attributs, généralement des propriétés. Les descripteurs sans données sont généralement utilisés dans les appels de méthode.
Les descripteurs de données qui déclenchent une erreur ʻAttributeError`` lorsque
`setest appelé sont appelés" descripteurs de données en lecture seule ". Les propriétés en lecture seule pour lesquelles
fset '' n'est pas défini sont classées comme des descripteurs de données en lecture seule plutôt que comme des descripteurs sans données.
Cette classification affecte la priorité d'accès aux attributs. Plus précisément, l'ordre de priorité est le suivant.
Nous verrons plus tard pourquoi il en est ainsi.
À ce stade, vous vous demandez peut-être en quoi les descripteurs et les propriétés sont différents.
Tout d'abord, la différence d'utilisation est que les propriétés sont généralement utilisées comme décorateurs dans les définitions de classe pour personnaliser l'accès aux attributs pour les instances de cette classe. Les descripteurs, par contre, sont définis indépendamment d'une classe particulière et sont utilisés pour personnaliser l'accès aux attributs pour d'autres classes.
Plus essentiellement, les propriétés sont un type de descripteur. En d'autres termes, le descripteur a une plus large gamme d'applications, et inversement, on peut dire que la propriété est spécialisée pour l'usage courant du descripteur.
Si vous écrivez X.Y '' dans votre code source, ce qui se passe en coulisses est compliqué, contrairement à sa simple apparence. En fait, ce qui se passe dépend du fait que
X '' est une classe ou une instance et que `` Y '' est une propriété, une méthode ou un attribut régulier.
Pour les attributs d'instance, cela signifie faire référence à la valeur correspondant à la clé spécifiée dans le dictionnaire d'attributs de l'instance `` dict ''.
class C(object):
def __init__(self):
self.x = 1
obj = C()
assert obj.x == obj.__dict__['x']
Pour les attributs de classe, cela signifie référencer les valeurs du dictionnaire d'attributs de la classe, à la fois via classe et via instance.
class C(object):
x = 1
assert C.x == C.__dict__['x']
obj = C()
assert obj.x == C.__dict__['x']
Jusqu'à présent, l'histoire est simple.
Dans le cas d'une propriété, il s'agit de la propriété elle-même lorsqu'elle est référencée à partir de la classe, et de la valeur de retour de la fonction lorsqu'elle est référencée à partir de l'instance.
class C(object):
@property
def x(self):
return 1
assert C.x == C.__dict__['x'].__get__(None, C)
#Propriété elle-même lorsqu'elle est référencée à partir de la classe
assert isinstance(C.x, property)
obj = C()
assert obj.x == C.__dict__['x'].__get__(obj, C)
#Valeur de retour de la fonction lorsqu'elle est référencée à partir d'une instance
assert obj.x == 1
Dans les coulisses, la méthode __get__ '' pour cet objet est appelée en recherchant la valeur dans le dictionnaire d'attributs de la classe. Des descripteurs sont utilisés dans cette partie. À ce stade, le premier argument de
getest
Aucun '' lorsque via une classe, et cette instance via une instance, et la valeur obtenue différera en fonction de cette différence.
Les méthodes sont fondamentalement les mêmes que les propriétés. Puisque le descripteur est appelé dans les coulisses, différentes valeurs seront obtenues lorsqu'il est référencé via une classe et lorsqu'il est référencé via une instance.
class C(object):
def x(self):
return 1
assert C.x == C.__dict__['x'].__get__(None, C)
obj = C()
assert obj.x == C.__dict__['x'].__get__(obj, C)
assert C.x != obj.x
Vous pouvez personnaliser tous les accès aux attributs d'une classe en remplaçant \ _ \ _ getattribute \ _ \ _. La différence, en revanche, est que les descripteurs vous permettent de personnaliser l'accès à des attributs spécifiques.
De plus, l'implémentation intégrée __getattribute__
prend en compte le descripteur, ce qui fait que le descripteur fonctionne comme prévu. C'est la relation essentielle.
Les classes typiques qui implémentent __getattribute__
sont ```object,
typeet
super``. Ici, nous comparerons «« objet »et« type ».
PyBaseObject_Type
correspondant au type `ʻobjectdans le Python [code source](http://hg.python.org/cpython/file/v3.4.1/Objects/typeobject.c#l4208) Puisque la structure est définie et que la fonction
PyObject_GenericGetAttrest spécifiée dans l'emplacement
tp_getattro, ```object .__ getattribute__
appelle cette fonction.
La définition de cette fonction se trouve dans Objects / object.c, qui est en pseudo-code Python. Cela ressemble à ceci:
def object_getattribute(self, key):
"Emulate PyObject_GenericGetAttr() in Objects/object.c"
tp = type(self)
attr = PyType_Lookup(tp, key)
if attr:
if hasattr(attr, '__get__') and hasattr(attr, '__set__'):
# data descriptor
return attr.__get__(self, tp)
if key in self.__dict__:
return self.__dict__[key]
if attr:
if hasattr(attr, '__get__'):
return attr.__get__(self, tp)
return attr
raise AttributeError
Il existe trois blocs principaux, 1) appelant le descripteur de données, 2) référençant le dictionnaire d'attributs de l'instance elle-même, et 3) appelant le descripteur sans données ou référençant le dictionnaire d'attributs de la classe.
Tout d'abord, récupérez la classe de l'objet et recherchez les attributs de cette classe. Pensez à PyType_Lookup '' comme une fonction qui parcourt une classe et sa classe parente et renvoie la valeur correspondant à la clé spécifiée à partir du dictionnaire d'attributs. Si l'attribut est trouvé ici et qu'il s'agit d'un descripteur de données, son
get '' sera appelé. Si le descripteur de données n'est pas trouvé, le dictionnaire d'attributs de l'instance est référencé et toutes les valeurs sont renvoyées. Enfin, il vérifie à nouveau l'attribut class, et s'il s'agit d'un descripteur, __get__ '' est appelé, sinon il retourne la valeur elle-même. Si aucune valeur n'est trouvée, `ʻAttributeError
est renvoyé.
De même, type .__ getattribute__
dans ```Objects / typeobject.c Il est défini dans la structure
PyType_Type '' (http://hg.python.org/cpython/file/v3.4.1/Objects/typeobject.c#l3122).
Ceci est exprimé en pseudo-code Python comme suit:
def type_getattribute(cls, key):
"Emulate type_getattro() in Objects/typeobject.c"
meta = type(cls)
metaattr = PyType_Lookup(meta, key)
if metaattr:
if hasattr(metaattr, '__get__') and hasattr(metaattr, '__set__'):
# data descriptor
return metaattr.__get__(cls, meta)
attr = PyType_Lookup(cls, key)
if attr:
if hasattr(attr, '__get__'):
return attr.__get__(None, cls)
return attr
if metaattr:
if hasattr(metaattr, '__get__'):
return metaattr.__get__(cls, meta)
return metaattr
raise AttributeError
La première moitié et la seconde moitié sont traitées de la même manière que pour ʻobject``, donc je vais l'omettre (notez que la classe de l'instance correspond à la métaclasse de la classe), mais le bloc du milieu est
C'est différent du cas d '«objet». Dans le cas de ʻobject``, c'était juste une référence au dictionnaire d'attributs, mais dans le cas d'une classe, il suit la classe parente pour faire référence au dictionnaire d'attributs, et s'il s'agit d'un descripteur, il appelle le descripteur
`get``. Je suis.
Pour résumer ce que nous avons vu jusqu'à présent
type (self)
pour ```objectet
clspour
typecomme argument de
get`` (Il est devenu)Par exemple, si vous avez un code comme celui-ci, même si vous mettez la valeur directement dans `` dict '', la propriété a la priorité.
class C(object):
@property
def x(self):
return 0
>>> o = C()
>>> o.__dict__['x'] = 1
>>> o.x
0
Regardons maintenant quelques exemples de descripteurs spécifiques.
Selon le protocole du descripteur, les propriétés peuvent être définies comme du code Python pur comme suit:
class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, obj, klass=None):
if obj is None:
# via class
return self
if self.fget is not None:
return self.fget(obj)
raise AttributeError
def __set__(self, obj, value):
if self.fset is not None:
self.fset(obj, value)
raise AttributeError
def __delete__(self, obj):
if self.fdel is not None:
self.fdel(obj)
raise AttributeError
Dans __get__
, si ʻobj`` vaut
`None, c'est-à-dire qu'il se retourne lorsqu'il est appelé via une classe. Si le
fgetpassé dans le constructeur n'est pas
None, il appelle
fget, et s'il est
None, il lance ```AttributeError
.
Le pseudo code pour `` staticmethod '' est le suivant.
class StaticMethod(object):
"Emulate PyStaticMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
return self.f
C'est facile, cela renvoie toujours la fonction elle-même lorsque __get__ '' est appelé. Par conséquent,
staticmethod`` se comporte de la même manière que la fonction d'origine, qu'elle soit appelée via une classe ou via une instance.
Le pseudo code de `` classmethod '' est le suivant.
class ClassMethod(object):
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
return types.MethodType(self.f, klass)
Lorsque __get__ '' est appelé, il crée et renvoie un objet
MethodType '' à partir de la fonction et de la classe. En réalité, le `` call '' de cet objet sera appelé immédiatement après.
Les méthodes d'instance sont en fait des fonctions. Par exemple, quand il y a une telle classe et fonction
class C(object):
pass
def f(self, x):
return x
L'appel de la fonction __get__
f
renverra un objet MethodType
. L'appel de ceci renverra le même résultat que si vous appeliez une méthode d'instance. Dans ce cas, f '' est une fonction qui n'a rien à voir avec la classe
C '', mais elle finit par être appelée comme une méthode.
obj = C()
# obj.f(1)Pour émuler
meth = f.__get__(obj, C)
assert isinstance(meth, types.MethodType)
assert meth(1) == 1
Cet exemple est une représentation plus extrême du fait qu'une fonction est un descripteur.
>>> def f(x, y): return x + y
...
>>> f
<function f at 0x10e51b1b8>
>>> f.__get__(1)
<bound method int.f of 1>
>>> f.__get__(1)(2)
3
La fonction f
définie ici est juste une fonction à deux arguments qui n'est ni une méthode ni quoi que ce soit, mais lorsque vous appelez cela `` get '', une méthode liée est renvoyée. Si vous lui passez un argument et que vous l'appelez, vous pouvez voir que l'appel de fonction est effectué. Comme vous pouvez le voir, toutes les fonctions sont des descripteurs, et lorsqu'ils sont appelés via une classe, les descripteurs agissent comme des méthodes.
Une méthode d'instance, c'est-à-dire une fonction en tant que descripteur, est représentée par un pseudo-code comme celui-ci.
class Function(object):
"Emulate PyFunction_Type() in Objects/funcobject.c"
def __get__(self, obj, klass=None):
if obj is None:
return self
return types.MethodType(self, obj)
Lorsqu'il est appelé via une classe, il se retourne lui-même, et lorsqu'il est appelé via une instance, il crée et retourne un objet MethodType
à partir de la fonction et de l'instance.
Le pseudo code pour MethodType .__ call__ '' est le suivant. Tout ce que vous avez à faire est de prendre
self '' et __func__ '' et d'ajouter
self '' au premier argument de la fonction pour appeler la fonction.
def method_call(meth, *args, **kw):
"Emulate method_call() in Objects/classobject.c"
self = meth.__self__
func = meth.__func__
return func(self, *args, **kw)
Pour résumer l'histoire jusqu'à présent,
obj.func(x)
L'appel de méthode équivaut au traitement suivant.
func = type(obj).__dict__['func']
meth = func.__get__(obj, type(obj))
meth.__call__(x)
C'est finalement équivalent à un appel de fonction comme celui-ci:
func(obj, x)
Allons-y un peu, mais réfléchissons à la raison pour laquelle le premier argument d'une méthode en Python est self ''. La raison peut être expliquée comme suit sur la base de l'histoire jusqu'à présent. En Python, l'entité d'une méthode d'instance est une fonction, et l'appel de la méthode d'instance est finalement converti en un simple appel de fonction par l'action du descripteur. Puisqu'il ne s'agit que d'une fonction, il est naturel de la passer comme argument lors du passage de l'équivalent de
self ''. Si le premier argument, `` self '', pouvait être omis, des conventions différentes devraient être utilisées pour les appels de fonction et les appels de méthode, ce qui compliquerait la spécification du langage. Je pense que la mécanique de Python consistant à utiliser des descripteurs pour convertir les appels de méthode en appels de fonction plutôt que de traiter les fonctions et les méthodes séparément est très intelligente.
Dans Python 3, si vous référencez une méthode d'instance via une classe, la fonction elle-même sera retournée, mais dans Python 2, la méthode indépendante sera retournée. Pour faire référence à la fonction elle-même, vous devez vous référer à l'attribut `` func ''. Cette écriture entraînera une erreur dans Python 3, donc soyez prudent lors du portage vers Python 3 si vous avez un code comme celui-ci. Dans Python 3, le concept de méthode non liée a disparu en premier lieu.
class C(object):
def f(self):
pass
$ python3
>>> C.f # == C.__dict__['f']
<function C.f at 0x10356ab00>
$ python2
>>> C.f # != C.__dict__['f']
<unbound method C.f>
>>> C.f.__func__ # == C.__dict__['f']
<function f at 0x10e02d050>
super
Un autre exemple où des descripteurs sont utilisés est `` super ''. Voir l'exemple ci-dessous.
class C(object):
def x(self):
pass
class D(C):
def x(self):
pass
class E(D):
pass
obj = E()
assert super(D, obj).x == C.__dict__['x'].__get__(obj, D)
Dans cet exemple, super (D, obj) .x
obtient la valeur correspondant à x
du dictionnaire d'attributs de la classe C
et met ```obj dans ce __get__
. Cela signifie appeler avec et
D '' comme arguments. Le point ici est que la classe qui obtient les attributs est C '' au lieu de
D ''. La clé est dans l'implémentation du __getattribute__ '' de la classe
super``.
Le pseudo code pour `` super .__ getattribute__ '' est le suivant.
def super_getattribute(su, key):
"Emulate super_getattro() in Objects/typeobject.c"
starttype = su.__self_class__
mro = iter(starttype.__mro__)
for cls in mro:
if cls is su.__self_class__:
break
# Note: mro is an iterator, so the second loop
# picks up where the first one left off!
for cls in mro:
if key in cls.__dict__:
attr = cls.__dict__[key]
if hasattr(attr, '__get__'):
return attr.__get__(su.__self__, starttype)
return attr
raise AttributeError
Recherche dans l'arborescence d'héritage mro la première classe spécifiée pour la classe "suivante" (ou "ci-dessus") de cette classe. Ensuite, à partir de ce point, le dictionnaire d'attributs est référencé lors du traçage de l'arbre d'héritage, et si l'attribut trouvé est un descripteur, le descripteur est appelé. C'est le mécanisme du `` super ''.
super
est aussi un descripteur. Cependant, cela ne semble pas être utilisé très efficacement en Python aujourd'hui. J'ai trouvé le seul code dans le code source Python dans le test: http://hg.python.org/cpython/file/v3.4.1/Lib/test/test_descr.py#l2308
Quand j'ai recherché, PEP 367 a suggéré une spécification appelée self.__super__.foo () '', peut-être Cela peut avoir quelque chose à voir avec cela. Au fait, ce PEP a finalement été adopté dans Python 3 sous le nom de [PEP 3135](http://legacy.python.org/dev/peps/pep-3135/), mais dans ce cas
super () ` Cette notation n'a pas été adoptée sous la forme que l'argument peut être omis.
reify
Enfin, voici un exemple de descripteur défini par l'utilisateur.
http://docs.pylonsproject.org/docs/pyramid/en/latest/_modules/pyramid/decorator.html#reify
C'est le code pour reify '' dans le framework Web Pyramid.
reifyest comme une propriété mise en cache et des fonctionnalités similaires existent dans d'autres frameworks, mais l'implémentation Pyramid est très intelligente avec des descripteurs. .. Le point est la partie où
setattrest exécuté dans la méthode
get. Ici, la valeur obtenue par l'appel de fonction est définie dans le dictionnaire d'attributs de l'instance, de sorte que l'appel de descripteur ne se produira pas la fois suivante. Étant donné que
reify '' est un descripteur sans données, le dictionnaire d'attributs de l'instance est prioritaire.
Recommended Posts