Dieser Artikel ist eine Zusammenstellung des Inhalts, der auf der PyCon JP 2014 vom 12. bis 14. September 2014 angekündigt wurde.
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.
__get__
als auch __set__
definiert __get__
definiertDatendeskriptoren 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.
Wir werden später mehr darüber erfahren, warum dies so ist.
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.
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.
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 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.
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.
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
__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
type (self)
für object
, cls
für type
als Argument für __get__
(Es ist geworden)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
Schauen wir uns nun einige spezifische Deskriptorbeispiele an.
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.
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.
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.
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.
Recommended Posts