Ich versuche zu verstehen, was die Deskriptoren von Python sind und wofür sie nützlich sein können.
Deskriptoren sind Objekte in einem Klassennamensraum, die Instanzattribute (wie Slots, Eigenschaften oder Methoden) verwalten. Zum Beispiel:
class HasDescriptors:
__slots__ = 'a_slot' # creates a descriptor
def a_method(self): # creates a descriptor
"a regular method"
@staticmethod # creates a descriptor
def a_static_method():
"a static method"
@classmethod # creates a descriptor
def a_class_method(cls):
"a class method"
@property # creates a descriptor
def a_property(self):
"a property"
# even a regular function:
def a_function(some_obj_or_self): # creates a descriptor
"create a function suitable for monkey patching"
HasDescriptors.a_function = a_function # (but we usually don't do this)
Pedantisch ausgedrückt, sind Deskriptoren Objekte mit cualquier der folgenden speziellen Methoden, die als "Deskriptormethoden" bezeichnet werden können:
__get__
Methode ohne Datenbeschreibung, z. B. für eine Methode/Funktion
__set__
Datenbeschreibungsmethode, z. B. für eine Eigenschaftsinstanz oder einen Steckplatz
__delete__
Daten-Deskriptor-Methode, die wiederum von Eigenschaften oder Slots verwendet wird
Diese Deskriptorobjekte sind Attribute in anderen Namespaces von Objektklassen. Das heißt, sie leben in der __dict__
des Klassenobjekts.
Deskriptor-Objekte verwalten programmatisch die Ergebnisse einer punktierten Suche (z. B. foo.descriptor
) in einem normalen Ausdruck, einer Zuweisung oder einer Löschung.
Funktionen/Methoden, gebundene Methoden, property
, classmethod
y staticmethod
verwenden alle diese speziellen Methoden, um zu steuern, wie auf sie über die punktierte Suche zugegriffen wird.
A Daten-Deskriptor , wie property
kann eine träge Bewertung von Attributen auf der Grundlage eines einfacheren Objektzustands ermöglichen, so dass die Instanzen weniger Speicherplatz benötigen, als wenn Sie jedes mögliche Attribut vorberechnen würden.
Eine weitere Daten-Deskriptor , a member_descriptor
erstellt von __slots__
ermöglicht Speicherplatzeinsparungen (und schnellere Suchvorgänge), indem die Klasse Daten in einer veränderbaren tupelähnlichen Datenstruktur speichert, anstatt in der flexibleren, aber platzraubenden __dict__
.
Nicht-Daten-Deskriptoren, Instanz- und Klassenmethoden, erhalten ihre impliziten ersten Argumente (in der Regel mit self
y cls
bzw.) aus ihrer Nicht-Daten-Deskriptor-Methode, __get__
- und so wissen statische Methoden, dass sie kein implizites erstes Argument haben dürfen.
Die meisten Python-Benutzer müssen nur die High-Level-Verwendung von Deskriptoren erlernen und haben keinen Bedarf, die Implementierung von Deskriptoren weiter zu erlernen oder zu verstehen.
Aber wenn man versteht, wie Deskriptoren funktionieren, kann man mehr Vertrauen in die eigene Beherrschung von Python gewinnen.
Vertiefung: Was sind Deskriptoren?
Ein Deskriptor ist ein Objekt mit einer der folgenden Methoden ( __get__
, __set__
o __delete__
), die über Dotted-Lookup wie ein typisches Attribut einer Instanz verwendet werden soll. Für ein Eigentümer-Objekt, obj_instance
, mit einer descriptor
Objekt:
-
obj_instance.descriptor
ruft auf.
descriptor.__get__(self, obj_instance, owner_class)
Rückgabe einer value
Auf diese Weise werden alle Methoden und die get
an einer Immobilie arbeiten.
-
obj_instance.descriptor = value
ruft auf.
descriptor.__set__(self, obj_instance, value)
zurückkehrend None
So wird die setter
auf eine Immobilie funktioniert.
-
del obj_instance.descriptor
ruft auf.
descriptor.__delete__(self, obj_instance)
zurückkehrend None
So wird die deleter
auf eine Immobilie funktioniert.
obj_instance
ist die Instanz, deren Klasse die Instanz des Deskriptorobjekts enthält. self
ist die Instanz der Deskriptor (wahrscheinlich nur eine für die Klasse der obj_instance
)
Um dies mit Code zu definieren, ist ein Objekt ein Deskriptor, wenn sich die Menge seiner Attribute mit einem der erforderlichen Attribute überschneidet:
def has_descriptor_attrs(obj):
return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))
def is_descriptor(obj):
"""obj can be instance of descriptor or the descriptor class"""
return bool(has_descriptor_attrs(obj))
A Daten Deskriptor a un __set__
und/oder __delete__
.
A Nicht-Daten-Deskriptor hat weder __set__
noch __delete__
.
def has_data_descriptor_attrs(obj):
return set(['__set__', '__delete__']) & set(dir(obj))
def is_data_descriptor(obj):
return bool(has_data_descriptor_attrs(obj))
Beispiele für eingebaute Deskriptor-Objekte:
classmethod
staticmethod
property
- Funktionen im Allgemeinen
Nicht-datenbezogene Deskriptoren
Wir können sehen, dass classmethod
y staticmethod
sind Nicht-Daten-Deskriptoren:
>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)
Beide haben nur die __get__
Methode:
>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))
Beachten Sie, dass alle Funktionen auch Nicht-Daten-Deskriptoren sind:
>>> def foo(): pass
...
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)
Daten Deskriptor, property
Allerdings, property
ist ein Daten-Deskriptor:
>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])
Gepunktete Lookup-Reihenfolge
Diese sind wichtig Unterscheidungen da sie die Suchreihenfolge bei einer punktierten Suche beeinflussen.
obj_instance.attribute
- Zunächst wird geprüft, ob das Attribut ein Data-Descriptor für die Klasse der Instanz ist,
- Wenn nicht, wird geprüft, ob das Attribut in der
obj_instance
's __dict__
dann
- fällt er schließlich auf einen Nicht-Daten-Deskriptor zurück.
Die Folge dieser Nachschlageordnung ist, dass Nicht-Daten-Deskriptoren wie Funktionen/Methoden überschrieben durch Instanzen .
Rekapitulation und nächste Schritte
Wir haben gelernt, dass Deskriptoren Objekte mit einer der folgenden Eigenschaften sind __get__
, __set__
o __delete__
. Diese Deskriptorobjekte können als Attribute für andere Objektklassendefinitionen verwendet werden. Jetzt werden wir uns ansehen, wie sie verwendet werden, indem wir Ihren Code als Beispiel verwenden.
Analyse des Codes aus der Frage
Hier ist Ihr Code, gefolgt von Ihren Fragen und den Antworten darauf:
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()
- Warum brauche ich die Deskriptorklasse?
Ihr Deskriptor stellt sicher, dass Sie immer einen Float für dieses Klassenattribut von Temperature
und dass Sie nicht mit del
um das Attribut zu löschen:
>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Andernfalls ignorieren Ihre Deskriptoren die Eigentümerklasse und die Instanzen des Eigentümers und speichern stattdessen den Zustand im Deskriptor. Mit einem einfachen Klassenattribut können Sie den Zustand genauso leicht für alle Instanzen freigeben (solange Sie es immer als Float auf die Klasse setzen und niemals löschen oder es den Benutzern Ihres Codes erlauben, dies zu tun):
class Temperature(object):
celsius = 0.0
Damit erhalten Sie genau das gleiche Verhalten wie in Ihrem Beispiel (siehe Antwort auf Frage 3 unten), verwenden aber ein Pythons builtin ( property
) und würde als idiomatischer angesehen werden:
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
- Was ist hier Instanz und Eigentümer? (in erhalten. ). Was ist der Zweck dieser Parameter?
instance
ist die Instanz des Besitzers, der den Deskriptor aufruft. Der Eigentümer ist die Klasse, in der das Deskriptorobjekt verwendet wird, um den Zugriff auf den Datenpunkt zu verwalten. In den Beschreibungen der speziellen Methoden, die Deskriptoren definieren, neben dem ersten Absatz dieser Antwort finden Sie weitere beschreibende Variablennamen.
- Wie würde ich dieses Beispiel aufrufen/verwenden?
Hier ist eine Demonstration:
>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>>
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0
Sie können das Attribut nicht löschen:
>>> del t2.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: __delete__
Und Sie können keine Variable zuweisen, die nicht in eine Fließkommazahl umgewandelt werden kann:
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02
Andernfalls handelt es sich um einen globalen Zustand für alle Instanzen, der durch Zuweisung an eine beliebige Instanz verwaltet wird.
Der erwartete Weg, den die meisten erfahrenen Python-Programmierer gehen würden, um dieses Ergebnis zu erreichen, wäre die Verwendung der property
Dekorator, der die gleichen Deskriptoren unter der Haube verwendet, aber das Verhalten in die Implementierung der Besitzerklasse einbringt (wiederum wie oben definiert):
class Temperature(object):
_celsius = 0.0
@property
def celsius(self):
return type(self)._celsius
@celsius.setter
def celsius(self, value):
type(self)._celsius = float(value)
Diese hat genau das gleiche erwartete Verhalten wie der ursprüngliche Code:
>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02
Schlussfolgerung
Wir haben die Attribute behandelt, die Deskriptoren definieren, den Unterschied zwischen Daten- und Nicht-Daten-Deskriptoren, eingebaute Objekte, die sie verwenden, und spezifische Fragen zur Verwendung.
Wie würden Sie also das Beispiel aus der Frage verwenden? Ich hoffe, Sie würden es nicht tun. Ich hoffe, Sie würden mit meinem ersten Vorschlag beginnen (ein einfaches Klassenattribut) und zum zweiten Vorschlag übergehen (der Eigenschaftsdekorator), wenn Sie es für notwendig halten.