Grundlegendes zu __get__ und __set__ und Python-Deskriptoren

310

Ich versuche zu verstehen, was Pythons Deskriptoren sind und wofür sie nützlich sind. Ich verstehe, wie sie funktionieren, aber hier sind meine Zweifel. Betrachten Sie den folgenden Code:

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()
  1. Warum brauche ich die Deskriptorklasse?

  2. Was ist instanceund ownerhier? (in __get__). Was ist der Zweck dieser Parameter?

  3. Wie würde ich dieses Beispiel aufrufen / verwenden?

Matt Bronson
quelle

Antworten:

147

Der Deskriptor gibt an, wie der Python- propertyTyp implementiert wird. Ein Descriptor einfach implementiert __get__, __set__usw. und wird dann in der Definition einer anderen Klasse hinzugefügt (wie Sie oben mit der Temperaturklasse tat). Zum Beispiel:

temp=Temperature()
temp.celsius #calls celsius.__get__

Wenn Sie auf die Eigenschaft zugreifen, der Sie den Deskriptor zugewiesen haben ( celsiusim obigen Beispiel), wird die entsprechende Deskriptormethode aufgerufen.

instancein __get__ist die Instanz der Klasse (würde so oben __get__empfangen temp, während ownerist die Klasse mit dem Deskriptor (so wäre es Temperature).

Sie müssen eine Deskriptorklasse verwenden, um die Logik zu kapseln, die sie antreibt. Auf diese Weise kann der Deskriptor, wenn er zum Beispiel zum Zwischenspeichern einer teuren Operation verwendet wird, den Wert in sich selbst und nicht in seiner Klasse speichern.

Einen Artikel über Deskriptoren finden Sie hier .

EDIT: Wie jchl in den Kommentaren betonte Temperature.celsius, instancewird es sein , wenn Sie es einfach versuchen None.

li.davidm
quelle
6
Was ist der Unterschied zwischen selfund instance?
Lemma Prism
2
'instance' kann eine Instanz einer beliebigen Klasse sein, self ist eine Instanz derselben Klasse.
TheBeginner
3
@LemmaPrism selfist die Deskriptorinstanz, instanceist die Instanz der Klasse (falls instanziiert), in der sich der Deskriptor befindet ( instance.__class__ is owner).
Tcll
Temperature.celsiusgibt den Wert 0.0gemäß dem Code an celsius = Celsius(). Der Deskriptor Celsius wird aufgerufen, daher hat seine Instanz den Init-Wert, 0.0der dem Attribut Temperaturklasse celsius zugewiesen ist.
Angel Salazar
109

Warum brauche ich die Deskriptorklasse?

Sie haben zusätzliche Kontrolle darüber, wie Attribute funktionieren. Wenn Sie beispielsweise in Java an Getter und Setter gewöhnt sind, ist dies Pythons Vorgehensweise. Ein Vorteil ist, dass es für Benutzer wie ein Attribut aussieht (die Syntax ändert sich nicht). Sie können also mit einem normalen Attribut beginnen und dann, wenn Sie etwas Besonderes tun müssen, zu einem Deskriptor wechseln.

Ein Attribut ist nur ein veränderbarer Wert. Mit einem Deskriptor können Sie beliebigen Code ausführen, wenn Sie einen Wert lesen oder festlegen (oder löschen). Sie können sich also vorstellen, damit ein Attribut beispielsweise einem Feld in einer Datenbank zuzuordnen - einer Art ORM.

Eine andere Verwendung könnte darin bestehen, die Annahme eines neuen Werts zu verweigern, indem eine Ausnahme __set__ausgelöst wird, wodurch das "Attribut" effektiv schreibgeschützt wird.

Was ist instanceund ownerhier? (in __get__). Was ist der Zweck dieser Parameter?

Dies ist ziemlich subtil (und der Grund, warum ich hier eine neue Antwort schreibe - ich fand diese Frage, während ich mich das Gleiche fragte, und fand die vorhandene Antwort nicht so großartig).

Ein Deskriptor ist für eine Klasse definiert, wird jedoch normalerweise von einer Instanz aufgerufen. Wann ist es von einer Instanz namens beiden instanceund ownersind so eingestellt (und Sie können ausrechnen owneraus , instanceso dass es irgendwie sinnlos scheint). Aber wenn es von einer Klasse aufgerufen wird, wird nur ownergesetzt - weshalb es da ist.

Dies wird nur benötigt, __get__weil es das einzige ist, das für eine Klasse aufgerufen werden kann. Wenn Sie den Klassenwert festlegen, legen Sie den Deskriptor selbst fest. Ähnliches gilt zum Löschen. Deshalb wird das ownerdort nicht benötigt.

Wie würde ich dieses Beispiel aufrufen / verwenden?

Hier ist ein cooler Trick mit ähnlichen Klassen:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(Ich verwende Python 3; für Python 2 müssen Sie sicherstellen, dass diese Unterteilungen / 5.0und sind / 9.0). Das gibt:

100.0
32.0

Jetzt gibt es andere, wohl bessere Möglichkeiten, um den gleichen Effekt in Python zu erzielen (z. B. wenn Celsius eine Eigenschaft wäre, die denselben grundlegenden Mechanismus darstellt, aber die gesamte Quelle in die Temperaturklasse einordnet), aber das zeigt, was getan werden kann ...

Andrew Cooke
quelle
2
Die Konvertierungen sind falsch: Sie sollten C = 5 (F - 32) / 9, F = 32 + 9C / 5 sein.
Musiphil
1
Stellen Sie sicher, dass Sie ein Temperaturobjekt haben. Das Folgen bringt das Zeug durcheinander. t1 = Temperatur (190) print t1.celsius t1.celsius = 100 print t1.fahrenheit Wenn Sie nun t.celcius und t.fahrenheit überprüfen, werden sie ebenfalls geändert. t.celcius ist 115 und t.fahrenheit ist 32. was eindeutig falsch ist. @ Eric
Ishan Bhatt
1
@IshanBhatt: Ich denke, das liegt an dem Fehler, auf den Musiphil oben hingewiesen hat. Auch nicht das ist nicht meine Antwort
Eric
69

Ich versuche zu verstehen, was Pythons Deskriptoren sind und wofür sie nützlich sein können.

Deskriptoren sind Klassenattribute (wie Eigenschaften oder Methoden) mit einer der folgenden speziellen Methoden:

  • __get__ (Nicht-Daten-Deskriptor-Methode, zum Beispiel für eine Methode / Funktion)
  • __set__ (Datendeskriptormethode, zum Beispiel für eine Eigenschaftsinstanz)
  • __delete__ (Datendeskriptormethode)

Diese Deskriptorobjekte können als Attribute für andere Objektklassendefinitionen verwendet werden. (Das heißt, sie leben im __dict__Objekt der Klasse.)

Deskriptorobjekte können verwendet werden, um die Ergebnisse einer gepunkteten Suche (z. B. foo.descriptor) in einem normalen Ausdruck, einer Zuweisung und sogar einer Löschung programmgesteuert zu verwalten .

Funktionen / Methoden, gebundene Methoden property, classmethodund staticmethodalle nutzt diese speziellen Methoden zur Kontrolle , wie sie über die gepunktete Lookup zugegriffen werden.

Ein Datendeskriptor wieproperty kann für faule Auswertung von Attributen auf einem einfacheren Zustand des Objekts ermöglichen basieren, so dass Instanzen als weniger Speicher verwenden , wenn Sie jedes mögliche Attribut vorberechnet.

Ein anderer Datendeskriptor , a member_descriptor, erstellt von __slots__, ermöglicht Speichereinsparungen, indem die Klasse Daten in einer veränderlichen tupelartigen Datenstruktur speichern kann, anstatt flexibler, aber platzaufwendiger __dict__.

Nicht-Daten - Deskriptoren, in der Regel beispielsweise Klasse und statische Methoden, erhalten ihre Argumente impliziten ersten ( in der Regel genannt clsund selfbezeichnet) von ihrer nicht-Datendeskriptor Methode __get__.

Die meisten Benutzer von Python müssen nur die einfache Verwendung lernen und müssen die Implementierung von Deskriptoren nicht weiter lernen oder verstehen.

Im Detail: Was sind Deskriptoren?

Ein Deskriptor ist ein Objekt mit einem der folgenden Verfahren ( __get__, __set__oder __delete__), bestimmt mittels gepunkteter lookup verwendet werden , als ob es ein typisches Merkmal einer Instanz waren. Für ein Eigentümerobjekt obj_instancemit einem descriptorObjekt:

  • obj_instance.descriptorruft die
    descriptor.__get__(self, obj_instance, owner_class)Rückgabe von a auf. value
    So funktionieren alle Methoden und die getEigenschaften einer Eigenschaft.

  • obj_instance.descriptor = valueruft die
    descriptor.__set__(self, obj_instance, value)Rückgabe auf None
    So funktioniert das setterauf einer Eigenschaft.

  • del obj_instance.descriptorruft die
    descriptor.__delete__(self, obj_instance)Rückgabe auf None
    So funktioniert das deleterauf einer Eigenschaft.

obj_instanceist die Instanz, deren Klasse die Instanz des Deskriptorobjekts enthält. selfist die Instanz des Deskriptors (wahrscheinlich nur eine für die Klasse derobj_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))

Ein Datendeskriptor hat ein __set__und / oder __delete__.
Ein 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 Deskriptorobjekte:

  • classmethod
  • staticmethod
  • property
  • Funktionen im Allgemeinen

Nicht-Daten-Deskriptoren

Wir können das sehen classmethodund staticmethodsind 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)

Datenbeschreibung, property

Ist propertyjedoch ein Daten-Deskriptor:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

Gepunktete Suchreihenfolge

Dies sind wichtige Unterscheidungen , da sie die Suchreihenfolge für eine gepunktete Suche beeinflussen.

obj_instance.attribute
  1. Zunächst wird geprüft, ob das Attribut ein Data-Descriptor für die Klasse der Instanz ist.
  2. Wenn nicht, wird geprüft, ob sich das Attribut in den obj_instance's __dict__befindet
  3. es fällt schließlich auf einen Nicht-Daten-Deskriptor zurück.

Die Folge dieser Suchreihenfolge ist, dass Nicht-Daten-Deskriptoren wie Funktionen / Methoden von Instanzen überschrieben werden können .

Rückblick und nächste Schritte

Wir haben gelernt , dass Deskriptoren sind Objekte mit einem __get__, __set__oder __delete__. Diese Deskriptorobjekte können als Attribute für andere Objektklassendefinitionen verwendet werden. Nun werden wir uns am Beispiel Ihres Codes ansehen, wie sie verwendet werden.


Analyse des Codes aus der Frage

Hier ist Ihr Code, gefolgt von Ihren Fragen und Antworten zu jedem:

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()
  1. Warum brauche ich die Deskriptorklasse?

Ihr Deskriptor stellt sicher, dass Sie immer ein Float für dieses Klassenattribut von Temperaturehaben und dass Sie deldas Attribut nicht löschen können:

>>> 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 Status im Deskriptor. Sie können den Status mit einem einfachen Klassenattribut genauso einfach für alle Instanzen freigeben (sofern Sie ihn immer als Float für die Klasse festlegen und ihn niemals löschen oder mit Benutzern Ihres Codes vertraut sind):

class Temperature(object):
    celsius = 0.0

Dadurch erhalten Sie genau das gleiche Verhalten wie in Ihrem Beispiel (siehe Antwort auf Frage 3 unten), verwenden jedoch ein integriertes Pythons ( property) und werden als idiomatischer angesehen:

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)
  1. Was ist Instanz und Eigentümer hier? (in get ). Was ist der Zweck dieser Parameter?

instanceist die Instanz des Besitzers, der den Deskriptor aufruft. Der Eigentümer ist die Klasse, in der das Deskriptorobjekt zum Verwalten des Zugriffs auf den Datenpunkt verwendet wird. Weitere beschreibende Variablennamen finden Sie in den Beschreibungen der speziellen Methoden, die Deskriptoren neben dem ersten Absatz dieser Antwort definieren.

  1. 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 einen Float konvertiert 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 haben Sie hier einen globalen Status für alle Instanzen, der durch Zuweisen zu einer beliebigen Instanz verwaltet wird.

Die erwartete Art und Weise, wie die meisten erfahrenen Python-Programmierer dieses Ergebnis erzielen würden, wäre die Verwendung des propertyDekorators, der dieselben Deskriptoren unter der Haube verwendet, aber das Verhalten in die Implementierung der Eigentümerklasse einbringt (wieder 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)

Welches hat genau das gleiche erwartete Verhalten des ursprünglichen Codeteils:

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

Fazit

Wir haben die Attribute behandelt, die Deskriptoren definieren, den Unterschied zwischen Daten- und Nicht-Daten-Deskriptoren, integrierte Objekte, die sie verwenden, und spezifische Fragen zur Verwendung.

Wie würden Sie das Beispiel der Frage verwenden? Ich hoffe du würdest nicht. Ich hoffe, Sie beginnen mit meinem ersten Vorschlag (einem einfachen Klassenattribut) und fahren mit dem zweiten Vorschlag (dem Eigenschaftsdekorateur) fort, wenn Sie dies für notwendig halten.

Aaron Hall
quelle
1
Schön, ich habe am meisten aus dieser Antwort gelernt (sicherlich auch von anderen). Eine Frage zu dieser Aussage "Die erwartete Art und Weise, wie die meisten erfahrenen Python-Programmierer dieses Ergebnis erzielen würden ...". Die Temeperature-Klasse, die Sie vor und nach der Anweisung definieren, ist identisch. Habe ich vermisst, was du hier vorhast?
Yolo Voe
1
@YoloVoe nein, das stimmt, ich habe einige Klammern hinzugefügt, um zu betonen, dass es sich um eine Wiederholung des oben Gesagten handelt.
Aaron Hall
1
Dies ist eine erstaunliche Antwort. Ich muss es noch ein paar Mal durchlesen, aber ich habe das Gefühl, dass mein Verständnis von Python nur um ein paar Punkte gestiegen ist
Lucas Young
20

Bevor Sie auf die Details der Deskriptoren eingehen, ist es möglicherweise wichtig zu wissen, wie die Attributsuche in Python funktioniert. Dies setzt voraus, dass die Klasse keine Metaklasse hat und die Standardimplementierung von verwendet __getattribute__(beide können zum "Anpassen" des Verhaltens verwendet werden).

Die beste Darstellung der Attributsuche (in Python 3.x oder für Klassen neuen Stils in Python 2.x) ist in diesem Fall das Verständnis der Python-Metaklassen (Ionels Codelog) . Das Bild wird :als Ersatz für "nicht anpassbare Attributsuche" verwendet.

Dies stellt die Suche eines Attributs foobarin einem instancevon dar Class:

Geben Sie hier die Bildbeschreibung ein

Zwei Bedingungen sind hier wichtig:

  • Wenn die Klasse von instanceeinen Eintrag für den Attributnamen hat __get__und und __set__.
  • Wenn das instancehat keinen Eintrag für die Attributnamen , aber die Klasse hat eine , und es hat __get__.

Hier kommen Deskriptoren ins Spiel:

  • Datendeskriptoren , die beide __get__und haben __set__.
  • Nicht-Daten-Deskriptoren, die nur haben __get__.

In beiden Fällen wird der zurückgegebene Wert __get__mit der Instanz als erstem Argument und der Klasse als zweitem Argument aufgerufen.

Die Suche ist noch komplizierter für die Suche nach Klassenattributen (siehe zum Beispiel die Suche nach Klassenattributen (im oben genannten Blog) ).

Kommen wir zu Ihren spezifischen Fragen:

Warum brauche ich die Deskriptorklasse?

In den meisten Fällen müssen Sie keine Deskriptorklassen schreiben! Sie sind jedoch wahrscheinlich ein sehr regelmäßiger Endbenutzer. Zum Beispiel Funktionen. Funktionen sind Deskriptoren. So können Funktionen als Methoden verwendet werden, die selfimplizit als erstes Argument übergeben werden.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Wenn Sie test_methodeine Instanz nachschlagen, erhalten Sie eine "gebundene Methode" zurück:

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

In ähnlicher Weise können Sie eine Funktion auch binden, indem Sie ihre __get__Methode manuell aufrufen (nicht wirklich empfohlen, nur zur Veranschaulichung):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

Sie können diese "selbstgebundene Methode" sogar aufrufen:

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

Beachten Sie, dass ich keine Argumente angegeben habe und die Funktion die von mir gebundene Instanz zurückgegeben hat!

Funktionen sind Nicht-Daten-Deskriptoren !

Einige eingebaute Beispiele für einen Datendeskriptor wären property. Vernachlässigen getter, setterund deleterder propertyDeskriptor ist (aus dem Descriptor HowTo Guide "Eigenschaften" ):

class Property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

Da es ein Datendeskriptor ist , ist es aufgerufen , wenn Sie den „Namen“ nachschlagen von der propertyund es einfach Delegierten zu den Funktionen dekoriert mit @property, @name.setterund@name.deleter (falls vorhanden).

In der Standardbibliothek gibt es mehrere andere Deskriptoren, z staticmethod.classmethod .

Der Punkt der Deskriptoren ist einfach (obwohl Sie sie selten benötigen): Abstrakter allgemeiner Code für den Attributzugriff. propertyist eine Abstraktion für den Zugriff auf Variablen, functionbietet eine Abstraktion für Methoden, staticmethodbietet eine Abstraktion für Methoden, die keinen Instanzzugriff benötigen, undclassmethod eine Abstraktion für Methoden, die Klassenzugriff anstelle von Instanzzugriff benötigen (dies ist etwas vereinfacht).

Ein anderes Beispiel wäre eine Klasseneigenschaft .

Ein lustiges Beispiel (mit __set_name__ Python 3.6) könnte auch eine Eigenschaft sein, die nur einen bestimmten Typ zulässt:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

Dann können Sie den Deskriptor in einer Klasse verwenden:

class Test(object):
    int_prop = TypedProperty(int)

Und ein bisschen damit spielen:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

Oder eine "faule Eigenschaft":

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

Dies sind Fälle, in denen das Verschieben der Logik in einen gemeinsamen Deskriptor sinnvoll sein könnte, man sie jedoch auch mit anderen Mitteln lösen könnte (aber möglicherweise durch Wiederholen eines Codes).

Was ist instanceund ownerhier? (in __get__). Was ist der Zweck dieser Parameter?

Dies hängt davon ab, wie Sie das Attribut nachschlagen. Wenn Sie das Attribut einer Instanz nachschlagen, gehen Sie wie folgt vor:

  • Das zweite Argument ist die Instanz, in der Sie das Attribut nachschlagen
  • Das dritte Argument ist die Klasse der Instanz

Wenn Sie das Attribut für die Klasse nachschlagen (vorausgesetzt, der Deskriptor ist für die Klasse definiert):

  • Das zweite Argument ist None
  • Das dritte Argument ist die Klasse, in der Sie das Attribut nachschlagen

Grundsätzlich ist das dritte Argument erforderlich, wenn Sie das Verhalten bei der Suche auf Klassenebene anpassen möchten (weil das instanceist None).

Wie würde ich dieses Beispiel aufrufen / verwenden?

Ihr Beispiel ist im Grunde eine Eigenschaft, die nur Werte zulässt, in die konvertiert werden kann floatund die von allen Instanzen der Klasse (und der Klasse gemeinsam genutzt werden) - obwohl man nur den Lesezugriff auf die Klasse verwenden kann, sonst würden Sie die Deskriptorinstanz ersetzen ):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

Aus diesem Grund verwenden Deskriptoren im Allgemeinen das zweite Argument ( instance), um den Wert zu speichern und eine gemeinsame Nutzung zu vermeiden. In einigen Fällen kann es jedoch erwünscht sein, einen Wert zwischen Instanzen zu teilen (obwohl ich mir derzeit kein Szenario vorstellen kann). Es macht jedoch praktisch keinen Sinn für eine Celsius-Eigenschaft in einer Temperaturklasse ... außer vielleicht als rein akademische Übung.

MSeifert
quelle
Ich bin mir nicht sicher, ob der transparente Hintergrund der Grafik, der wirklich im dunklen Modus leidet, als Fehler beim Stapelüberlauf gemeldet werden sollte.
Tshirtman
@Tshirtman Ich denke, das ist ein Problem mit dem Bild selbst. Es ist nicht vollständig transparent ... Ich habe es aus dem Blog-Beitrag genommen und weiß nicht, wie ich es mit dem richtigen transparenten Hintergrund neu erstellen soll. Es ist schade, dass es mit dem dunklen Hintergrund so komisch aussieht :(
MSeifert
9

Warum brauche ich die Deskriptorklasse?

Inspiriert von Fluent Python von Buciano Ramalho

Stellen Sie sich vor, Sie haben eine Klasse wie diese

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

Wir sollten das Gewicht und den Preis validieren, um zu vermeiden, dass ihnen eine negative Zahl zugewiesen wird. Wir können weniger Code schreiben, wenn wir den Deskriptor als Proxy verwenden

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

Definieren Sie dann die Klasse LineItem wie folgt:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

und wir können die Mengenklasse erweitern, um eine allgemeinere Validierung durchzuführen

wllbll
quelle
1
Interessanter Anwendungsfall, da er zeigt, wie der Deskriptor für die Interaktion mit mehreren Benutzerinstanzen verwendet wird. Ich habe den wichtigen Punkt anfangs nicht verstanden: Ein Attribut mit einem Deskriptor muss im Klassennamensraum erstellt werden (z. B. weight = Quantity()müssen Werte im Instanznamensraum nur mit self(z. B. self.weight = 4) festgelegt werden, sonst würde das Attribut auf den neuen Wert zurückgesetzt und der Deskriptor würde verworfen. Schön!
Minuten
Ich kann eins nicht verstehen. Sie definieren weight = Quantity()als Klassenvariable und ihre __get__und __set__arbeiten an der Instanzvariablen. Wie?
Technocrat
0

Ich habe (mit geringfügigen Änderungen wie vorgeschlagen) den Code aus Andrew Cookes Antwort ausprobiert. (Ich verwende Python 2.7).

Der Code:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

Das Ergebnis:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

Stellen Sie bei Python vor 3 sicher, dass Sie eine Unterklasse von einem Objekt haben, damit der Deskriptor ordnungsgemäß funktioniert, da get get magic für Klassen im alten Stil nicht funktioniert.

Gregory Kuhn
quelle
1
Deskriptoren arbeiten nur mit neuen Stilklassen. Für Python 2.x bedeutet dies, dass Sie Ihre Klasse von "Objekt" ableiten, was in Python 3 standardmäßig der Fall ist.
Ivo van der Wijk
0

Sie sehen https://docs.python.org/3/howto/descriptor.html#properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)
Yonks Somarl
quelle
1
Dies beantwortet weder die Frage noch liefert es nützliche Informationen.
Sebastian Nielsen