Technologie prenant en charge le descripteur Python #pyconjp

introduction

Cet article est une compilation du contenu annoncé à PyCon JP 2014 qui s'est tenue du 12 au 14 septembre 2014.

Qu'est-ce qu'un descripteur?

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.

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.

  1. Descripteur de données
  2. Dictionnaire d'attributs d'instance
  3. Descripteur sans données

Nous verrons plus tard pourquoi il en est ainsi.

Différence de propriété

À 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.

Que se passe-t-il derrière `` X.Y ''

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

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

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.

Pour les propriétés

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.

Pour les méthodes

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

Relation entre les descripteurs et `` getattribute ''

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

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

Exemple spécifique de descripteur

Regardons maintenant quelques exemples de descripteurs spécifiques.

Propriété

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.

Méthode statique

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.

Méthode de classe

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.

Méthode d'instance

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.

Résumé

Recommended Posts

Technologie prenant en charge le descripteur Python #pyconjp
Notez qu'il prend en charge Python 3
Prend en charge Python 2.4
descripteur python
Python: créer une classe qui prend en charge l'affectation décompressée
Outil MALSS (application) qui prend en charge l'apprentissage automatique en Python
Outil MALSS (basique) qui prend en charge l'apprentissage automatique en Python
MALSS (introduction), un outil qui prend en charge l'apprentissage automatique en Python
Algorithme A * (édition Python)
Première 3e édition de Python
Création d'un script Python prenant en charge l'API e-Stat (ver.2)
Une technologie qui soutient les créateurs de caca ~ La transition d'état rêve-t-elle de caca?
Technologie qui prend en charge Jupyter: Tralets (histoire d'essayer de déchiffrer)
Comment écrire une classe méta qui prend en charge à la fois python2 et python3