Technologie, die Python Descriptor #pyconjp unterstützt

Einführung

Dieser Artikel ist eine Zusammenstellung des Inhalts, der auf der PyCon JP 2014 vom 12. bis 14. September 2014 angekündigt wurde.

Was ist ein Deskriptor?

Ein Deskriptor ist ein Objekt, das die folgenden Methoden definiert.

class Descriptor(object):
    def __get__(self, obj, type=None): pass
    def __set__(self, obj, value): pass
    def __delete__(self, obj): pass

In Python wird eine Reihe von Methoden, die ein Objekt mit einer bestimmten Eigenschaft implementieren sollte, als Protokoll bezeichnet (ein typisches Protokoll ist das Iterator-Protokoll (http://docs.python.jp/3.4/library/stdtypes.). html # typeiter) etc.). Deskriptoren sind ein solches Protokoll.

Dieser Deskriptor wird hinter grundlegenden Python-Funktionen wie Eigenschaften, Methoden (statische Methoden, Klassenmethoden, Instanzmethoden) und super verwendet. Deskriptoren sind ebenfalls ein generisches Protokoll und können benutzerdefiniert werden.

Es gibt zwei Haupttypen von Deskriptoren.

Datendeskriptoren verhalten sich wie normaler Attributzugriff, normalerweise Eigenschaften. Nicht-Daten-Deskriptoren werden normalerweise in Methodenaufrufen verwendet.

Datendeskriptoren, die beim Aufruf von __set__ einen AttributeError auslösen, werden als "schreibgeschützte Datendeskriptoren" bezeichnet. Schreibgeschützte Eigenschaften, für die fset nicht definiert ist, werden als schreibgeschützte Datendeskriptoren und nicht als Nicht-Datendeskriptoren klassifiziert.

Diese Klassifizierung wirkt sich auf die Priorität des Attributzugriffs aus. Insbesondere ist die Prioritätsreihenfolge wie folgt.

  1. Datendeskriptor
  2. Instanzattributwörterbuch
  3. Nicht-Daten-Deskriptor

Wir werden später mehr darüber erfahren, warum dies so ist.

Unterschied zum Eigentum

An diesem Punkt fragen Sie sich möglicherweise, wie sich Deskriptoren und Eigenschaften unterscheiden.

Erstens besteht der Unterschied in der Verwendung darin, dass Eigenschaften normalerweise als Dekoratoren in Klassendefinitionen verwendet werden, um den Attributzugriff für Instanzen dieser Klasse anzupassen. Deskriptoren hingegen werden unabhängig von einer bestimmten Klasse definiert und zum Anpassen des Attributzugriffs für andere Klassen verwendet.

Im Wesentlichen sind Eigenschaften eine Art Deskriptor. Mit anderen Worten, Deskriptoren haben einen breiteren Anwendungsbereich, und umgekehrt kann gesagt werden, dass Eigenschaften auf die allgemeine Verwendung von Deskriptoren spezialisiert sind.

Was ist hinter X.Y los?

Wenn Sie "X.Y" in Ihren Quellcode schreiben, ist das, was hinter den Kulissen passiert, im Gegensatz zu seinem einfachen Erscheinungsbild kompliziert. Tatsächlich hängt das, was passiert, davon ab, ob "X" eine Klasse oder Instanz ist und ob "Y" eine Eigenschaft, eine Methode oder ein reguläres Attribut ist.

Zum Beispiel Attribute

Für Instanzattribute bedeutet dies, dass auf den Wert verwiesen wird, der dem angegebenen Schlüssel aus dem Attributwörterbuch der Instanz "dict" entspricht.

class C(object):
    def __init__(self):
        self.x = 1
  
obj = C()
assert obj.x == obj.__dict__['x']

Für Klassenattribute

Für Klassenattribute bedeutet dies, dass Werte aus dem Attributwörterbuch der Klasse sowohl über die Klasse als auch über die Instanz referenziert werden.

class C(object):
    x = 1

assert C.x == C.__dict__['x']

obj = C()
assert obj.x == C.__dict__['x']

Bisher ist die Geschichte einfach.

Für Eigenschaften

Im Fall einer Eigenschaft ist dies die Eigenschaft selbst, wenn von der Klasse verwiesen wird, und der Rückgabewert der Funktion, wenn von der Instanz verwiesen wird.

class C(object):
    @property
    def x(self):
        return 1

assert C.x == C.__dict__['x'].__get__(None, C)
#Eigenschaft selbst, wenn von der Klasse verwiesen wird
assert isinstance(C.x, property)

obj = C()
assert obj.x == C.__dict__['x'].__get__(obj, C)
#Funktionsrückgabewert, wenn von einer Instanz verwiesen wird
assert obj.x == 1

Hinter den Kulissen wird die Methode __get__ für dieses Objekt aufgerufen, indem der Wert aus dem Attributwörterbuch der Klasse nachgeschlagen wird. In diesem Teil werden Deskriptoren verwendet. Zu diesem Zeitpunkt ist das erste Argument von __get__ None über eine Klasse und diese Instanz über eine Instanz, und der erhaltene Wert unterscheidet sich in Abhängigkeit von diesem Unterschied.

Für Methoden

Methoden sind grundsätzlich die gleichen wie Eigenschaften. Da der Deskriptor hinter den Kulissen aufgerufen wird, werden unterschiedliche Werte erhalten, wenn über eine Klasse und über eine Instanz verwiesen wird.

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

Beziehung zwischen Deskriptoren und __getattribute__

Sie können den gesamten Attributzugriff für Ihre Klasse anpassen, indem Sie \ _ \ _ getattribute \ _ \ _ überschreiben. Der Unterschied besteht andererseits darin, dass Sie mit Deskriptoren den spezifischen Attributzugriff anpassen können.

Darüber hinaus berücksichtigt die integrierte Implementierung __getattribute__ den Deskriptor, was dazu führt, dass der Deskriptor wie beabsichtigt funktioniert. Dies ist die wesentliche Beziehung.

Typische Klassen, die __getattribute__ implementieren, sind object, type und super. Hier werden wir object und type vergleichen.

PyBaseObject_Type entspricht dem Objekt Typ in Python Quellcode Object .__ getattribute__ ruft diese Funktion auf, da die Struktur definiert ist und die Funktion PyObject_GenericGetAttr im Slot tp_getattro angegeben ist.

Die Definition dieser Funktion finden Sie in Objects / object.c, das sich im Python-Pseudocode befindet. Es sieht aus wie das:


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

Es gibt drei Hauptblöcke: 1) Aufrufen des Datendeskriptors, 2) Verweisen auf das Attributwörterbuch der Instanz selbst und 3) Aufrufen des Nicht-Datendeskriptors oder Verweisen auf das Attributwörterbuch der Klasse.

Holen Sie sich zunächst die Klasse des Objekts und suchen Sie nach den Attributen dieser Klasse. Stellen Sie sich PyType_Lookup als eine Funktion vor, die eine Klasse und ihre übergeordnete Klasse durchläuft und den Wert zurückgibt, der dem angegebenen Schlüssel aus dem Attributwörterbuch entspricht. Wenn das Attribut hier gefunden wird und es sich um einen Datendeskriptor handelt, wird sein __get__ aufgerufen. Wenn der Datendeskriptor nicht gefunden wird, wird auf das Attributwörterbuch der Instanz verwiesen und alle Werte werden zurückgegeben. Schließlich wird erneut nach dem Klassenattribut gesucht, und wenn es sich um einen Deskriptor handelt, wird __get__ aufgerufen, andernfalls wird der Wert selbst zurückgegeben. Wenn kein Wert gefunden wird, wird AttributeError ausgelöst.

In ähnlicher Weise geben Sie .__ getattribute__ in Objects / typeobject.c ein Es ist in der PyType_Type -Struktur definiert (http://hg.python.org/cpython/file/v3.4.1/Objects/typeobject.c#l3122).

Dies wird im Python-Pseudocode wie folgt ausgedrückt:


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

Die erste und die zweite Hälfte werden auf die gleiche Weise wie für "Objekt" verarbeitet, daher werde ich sie weglassen (beachten Sie, dass die Klasse für die Instanz der Metaklasse für die Klasse entspricht), aber der mittlere Block ist " Dies unterscheidet sich vom Fall von "Objekt". Im Fall von "Objekt" war es nur eine Referenz auf das Attributwörterbuch, aber im Fall einer Klasse folgt es der übergeordneten Klasse, um auf das Attributwörterbuch zu verweisen, und wenn es ein Deskriptor ist, ruft es den Deskriptor "get" auf. Ich bin.

Um zusammenzufassen, was wir bisher gesehen haben

Wenn Sie beispielsweise über einen solchen Code verfügen, hat die Eigenschaft Priorität, auch wenn Sie den Wert direkt in "dict" eingeben.

class C(object):
    @property
    def x(self):
        return 0

>>> o = C()
>>> o.__dict__['x'] = 1
>>> o.x
0

Spezifisches Beispiel eines Deskriptors

Schauen wir uns nun einige spezifische Deskriptorbeispiele an.

Eigentum

Gemäß dem Deskriptorprotokoll können Eigenschaften wie folgt als reiner Python-Code definiert werden:

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

Wenn in __get__ obj None ist, gibt es sich selbst zurück, wenn es über eine Klasse aufgerufen wird. Wenn das im Konstruktor übergebene fget nicht None ist, wird fget aufgerufen, und wenn es None ist, wird AttributeError ausgelöst.

Statische Methode

Der Pseudocode für "statische Methode" lautet wie folgt.

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

Dies ist einfach, es gibt immer die Funktion selbst zurück, wenn __get__ aufgerufen wird. Daher verhält sich staticmethod genauso wie die ursprüngliche Funktion, unabhängig davon, ob sie über eine Klasse oder über eine Instanz aufgerufen wird.

Klassenmethode

Der Pseudocode für "Klassenmethode" lautet wie folgt.

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)

Wenn __get__ aufgerufen wird, wird ein MethodType-Objekt aus der Funktion und Klasse erstellt und zurückgegeben. In der Realität wird der __call__ dieses Objekts unmittelbar danach aufgerufen.

Instanzmethode

Instanzmethoden sind eigentlich Funktionen. Zum Beispiel, wenn es eine solche Klasse und Funktion gibt

class C(object):
    pass

def f(self, x):
    return x

Das Aufrufen der Funktion __get__ f gibt ein MethodType Objekt zurück. Wenn Sie dies aufrufen, wird das gleiche Ergebnis zurückgegeben, als würden Sie eine Instanzmethode aufrufen. In diesem Fall ist f eine Funktion, die nichts mit der Klasse C zu tun hat, aber am Ende als Methode aufgerufen wird.

obj = C()
# obj.f(1)Emulieren
meth = f.__get__(obj, C)
assert isinstance(meth, types.MethodType)
assert meth(1) == 1

Dieses Beispiel ist eine extremere Darstellung der Tatsache, dass eine Funktion ein Deskriptor ist.

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

Die hier definierte Funktion f ist nur eine Funktion mit zwei Argumenten, die weder eine Methode noch irgendetwas ist. Wenn Sie diese __get__ aufrufen, wird eine gebundene Methode zurückgegeben. Wenn Sie ihm ein Argument übergeben und es aufrufen, können Sie sehen, dass der Funktionsaufruf erfolgt. Wie Sie sehen können, sind alle Funktionen Deskriptoren, und wenn sie über eine Klasse aufgerufen werden, fungieren die Deskriptoren als Methoden.

Eine Instanzmethode, dh eine Funktion als Deskriptor, wird durch einen solchen Pseudocode dargestellt.

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)

Wenn es über eine Klasse aufgerufen wird, gibt es sich selbst zurück, und wenn es über eine Instanz aufgerufen wird, erstellt und gibt es ein MethodType-Objekt aus der Funktion und Instanz zurück.

Der Pseudocode für MethodType .__ call__ lautet wie folgt. Alles, was Sie tun müssen, ist, __self__ und __func__ zu nehmen und self zum ersten Argument der Funktion hinzuzufügen, um die Funktion aufzurufen.

def method_call(meth, *args, **kw):
    "Emulate method_call() in Objects/classobject.c"
    self = meth.__self__
    func = meth.__func__
    return func(self, *args, **kw)

Um die bisherige Geschichte zusammenzufassen:

obj.func(x)

Der Methodenaufruf entspricht der folgenden Verarbeitung.

func = type(obj).__dict__['func']
meth = func.__get__(obj, type(obj))
meth.__call__(x)

Dies entspricht letztendlich einem Funktionsaufruf wie diesem:

func(obj, x)

Lassen Sie uns hier ein wenig davon abkommen, aber denken wir darüber nach, warum das erste Argument einer Methode in Python "Selbst" ist. Der Grund kann anhand der bisherigen Geschichte wie folgt erklärt werden. In Python ist die Entität einer Instanzmethode eine Funktion, und der Aufruf der Instanzmethode wird schließlich durch die Aktion des Deskriptors in einen einfachen Funktionsaufruf konvertiert. Da es sich nur um eine Funktion handelt, ist es natürlich, sie als Argument zu übergeben, wenn das Äquivalent von "Selbst" übergeben wird. Wenn das erste Argument "self" weggelassen werden könnte, müssten für Funktionsaufrufe und Methodenaufrufe unterschiedliche Konventionen verwendet werden, was die Sprachspezifikation kompliziert. Ich denke, Pythons Mechanik, Deskriptoren zu verwenden, um Methodenaufrufe in Funktionsaufrufe umzuwandeln, anstatt Funktionen und Methoden getrennt zu behandeln, ist sehr klug.

Wenn Sie in Python 3 über eine Klasse auf eine Instanzmethode verweisen, wird die Funktion selbst zurückgegeben, in Python 2 wird die ungebundene Methode zurückgegeben. Um auf die Funktion selbst zu verweisen, müssen Sie auf das Attribut __func__ verweisen. Dieses Schreiben führt zu einem Fehler in Python 3. Seien Sie also vorsichtig, wenn Sie auf Python 3 portieren, wenn Sie Code wie diesen haben. In Python 3 ist das Konzept der ungebundenen Methode überhaupt verschwunden.

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

Ein anderes Beispiel, in dem Deskriptoren verwendet werden, ist "super". Siehe das folgende Beispiel.

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)

In diesem Beispiel erhält super (D, obj) .x den Wert, der x entspricht, aus dem Attributwörterbuch der Klasse C und fügt obj in dieses getein. Es bedeutet, mit und D als Argumenten aufzurufen. Der Punkt hier ist, dass die Klasse, die die Attribute erhält, "C" anstelle von "D" ist. Der Schlüssel liegt in der Implementierung des __getattribute__ der super Klasse.

Der Pseudocode für super .__ getattribute__ lautet wie folgt.

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

Durchsucht den mro-Vererbungsbaum nach der ersten angegebenen Klasse nach der "nächsten" (oder "über") Klasse dieser Klasse. Ab diesem Punkt wird dann auf das Attributwörterbuch verwiesen, während der Vererbungsbaum verfolgt wird. Wenn das gefundene Attribut ein Deskriptor ist, wird der Deskriptor aufgerufen. Dies ist der Mechanismus von "Super".

super ist auch ein Deskriptor. Dies scheint jedoch in Python heute nicht sehr effektiv eingesetzt zu werden. Ich habe den einzigen Code im Python-Quellcode im Test gefunden: http://hg.python.org/cpython/file/v3.4.1/Lib/test/test_descr.py#l2308

Als ich es nachgeschlagen habe, schlug PEP 367 eine Spezifikation mit dem Namen self.__super__.foo () vor Es kann etwas damit zu tun haben. Übrigens wurde dieses PEP schließlich in Python 3 als PEP 3135 übernommen, aber in diesem Fall "super ()" Diese Notation wurde nicht in der Form übernommen, dass das Argument weggelassen werden kann.

reify

Schließlich ist hier ein Beispiel eines benutzerdefinierten Deskriptors.

http://docs.pylonsproject.org/docs/pyramid/en/latest/_modules/pyramid/decorator.html#reify

Dies ist der Code für "reify" in der Webframework-Pyramide. reify ist wie eine zwischengespeicherte Eigenschaft und ähnliche Funktionen gibt es in anderen Frameworks, aber die Pyramid-Implementierung ist mit Deskriptoren sehr intelligent. .. Der Punkt ist der Teil, an dem setattr in der __get__ Methode ausgeführt wird. Hier wird der durch den Funktionsaufruf erhaltene Wert im Attributwörterbuch der Instanz festgelegt, damit der Deskriptoraufruf beim nächsten Mal nicht mehr auftritt. Da reify ein Nicht-Daten-Deskriptor ist, hat das Attributwörterbuch der Instanz Vorrang.

Zusammenfassung

Recommended Posts

Technologie, die Python Descriptor #pyconjp unterstützt
Beachten Sie, dass es Python 3 unterstützt
Unterstützt Python 2.4
Python-Deskriptor
Python: Erstellen Sie eine Klasse, die entpackte Zuweisungen unterstützt
Tool MALSS (Anwendung), das maschinelles Lernen in Python unterstützt
Tool MALSS (Basic), das maschinelles Lernen in Python unterstützt
MALSS (Einführung), ein Tool, das maschinelles Lernen in Python unterstützt
Ein * Algorithmus (Python Edition)
Erste Python 3rd Edition
Erstellen eines Python-Skripts, das die e-Stat-API unterstützt (Version 2)
Technologie, die Poop-Macher unterstützt ~ Träumt der Staatsübergang von Poop?
Technologie, die Jupiter unterstützt: Traitlets (Geschichte des Entschlüsselungsversuchs)
So schreiben Sie eine Meta-Klasse, die sowohl Python2 als auch Python3 unterstützt