403 Stimmen

Verstehen von __get__ und __set__ und Python-Deskriptoren

Ich bin Versuch um zu verstehen, was die Deskriptoren von Python 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 instance y owner hier? (in __get__ ). Was ist der Zweck dieser Parameter?

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

198voto

li.davidm Punkte 10755

Der Deskriptor ist wie Pythons property Typ implementiert ist. Ein Deskriptor implementiert einfach __get__ , __set__ usw. und wird dann einer anderen Klasse in deren Definition hinzugefügt (wie Sie es oben mit der Klasse Temperatur getan haben). Zum Beispiel:

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

Der Zugriff auf die Eigenschaft, der Sie den Deskriptor zugewiesen haben ( celsius im obigen Beispiel) ruft die entsprechende Deskriptormethode auf.

instance sur __get__ ist die Instanz der Klasse (siehe oben, __get__ erhalten würde temp , während owner ist die Klasse mit dem Deskriptor (es wäre also Temperature ).

Sie müssen eine Deskriptorklasse verwenden, um die Logik zu kapseln, mit der sie arbeitet. Wenn der Deskriptor zum Zwischenspeichern einer teuren Operation verwendet wird (z. B.), kann er den Wert in sich selbst und nicht in seiner Klasse speichern.

Ein Artikel über Deskriptoren findet sich unter aquí .

EDIT: Wie jchl in den Kommentaren bemerkte, kann man einfach versuchen Temperature.celsius , instance wird None .

146voto

andrew cooke Punkte 43891

Warum brauche ich die Deskriptorklasse?

Damit haben Sie zusätzliche Kontrolle darüber, wie Attribute funktionieren. Wenn Sie z. B. an Getter und Setter in Java gewöhnt sind, dann ist das die Art und Weise, wie Python das macht. Ein Vorteil ist, dass es für die Benutzer genau wie ein Attribut aussieht (die Syntax ändert sich nicht). Sie können also mit einem gewöhnlichen Attribut beginnen und dann, wenn Sie etwas Ausgefallenes tun müssen, zu einem Deskriptor wechseln.

Ein Attribut ist lediglich ein veränderbarer Wert. Mit einem Deskriptor können Sie beim Lesen oder Setzen (oder Löschen) eines Wertes beliebigen Code ausführen. Man kann sich also vorstellen, dass man damit z. B. ein Attribut auf ein Feld in einer Datenbank abbildet - eine Art ORM.

Eine andere Verwendung könnte die Verweigerung der Annahme eines neuen Wertes durch das Auslösen einer Ausnahme in __set__ - Das "Attribut" ist somit schreibgeschützt.

Was ist instance y owner hier? (in __get__ ). Was ist der Zweck dieser Parameter?

Das ist ziemlich subtil (und der Grund, warum ich hier eine neue Antwort schreibe - ich habe diese Frage gefunden, als ich mich dasselbe fragte und fand die vorhandene Antwort nicht so toll).

Ein Deskriptor ist für eine Klasse definiert, wird aber normalerweise von einer Instanz aufgerufen. Wenn er von einer Instanz aufgerufen wird, werden sowohl instance y owner eingestellt sind (und Sie können ausrechnen owner de instance also scheint es ziemlich sinnlos zu sein). Aber wenn es von einer Klasse aufgerufen wird, wird nur owner eingestellt ist - und deshalb ist sie da.

Dies ist nur erforderlich für __get__ weil sie die einzige ist, die bei einer Klasse aufgerufen werden kann. Wenn Sie den Klassenwert setzen, setzen Sie den Deskriptor selbst. Ähnliches gilt für das Löschen. Aus diesem Grund ist der owner ist dort nicht erforderlich.

Wie würde ich dieses Beispiel aufrufen/verwenden?

Nun, hier ist ein cooler Trick, der ähnliche Klassen verwendet:

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 die Divisionen / 5.0 y / 9.0 ). Das ergibt:

100.0
32.0

Jetzt gibt es andere, wohl bessere Wege, um den gleichen Effekt in Python zu erreichen (z.B. wenn Celsius eine Eigenschaft wäre, was der gleiche grundlegende Mechanismus ist, aber die ganze Quelle innerhalb der Temperaturklasse platziert), aber das zeigt, was getan werden kann...

103voto

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
  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 das Attribut in der obj_instance 's __dict__ dann
  3. 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()
  1. 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)
  1. 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.

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

33voto

MSeifert Punkte 131411

Bevor wir auf die Details der Deskriptoren eingehen, ist es vielleicht wichtig zu wissen, wie die Attributsuche in Python funktioniert. Dies setzt voraus, dass die Klasse keine Metaklasse hat und die Standardimplementierung von __getattribute__ (beide können verwendet werden, um das Verhalten "anzupassen").

Die beste Veranschaulichung der Attributssuche (in Python 3.x oder für New-Style-Klassen in Python 2.x) in diesem Fall ist aus Verständnis der Python-Metaklassen (ionel's codelog) . Das Bild verwendet : als Ersatz für "nicht anpassbares Attribut lookup".

Dies stellt die Suche nach einem Attribut dar foobar an einem instance von Class :

enter image description here

Zwei Bedingungen sind hier wichtig:

  • Wenn die Klasse der instance hat einen Eintrag für das Attribut name und es hat __get__ y __set__ .
  • Wenn die instance hat keine Eintrag für das Attribut name, aber die Klasse hat einen und dieser hat __get__ .

Hier kommen die Deskriptoren ins Spiel:

  • Daten Deskriptoren die beide über __get__ y __set__ .
  • Nicht-datenbezogene Deskriptoren die nur über __get__ .

In beiden Fällen geht der zurückgegebene Wert durch __get__ mit der Instanz als erstem Argument und der Klasse als zweitem Argument aufgerufen.

Bei der Suche nach Klassenattributen ist die Suche noch komplizierter (siehe zum Beispiel Suche nach Klassenattributen (im oben erwähnten Blog) ).

Kommen wir nun zu Ihren konkreten Fragen:

Warum brauche ich die Deskriptorklasse?

In den meisten Fällen brauchen Sie keine Deskriptorklassen zu schreiben! Allerdings sind Sie wahrscheinlich ein sehr regelmäßiger Endbenutzer. Zum Beispiel Funktionen. Funktionen sind Deskriptoren, d.h. Funktionen können als Methoden verwendet werden mit self implizit als erstes Argument übergeben.

def test_function(self):
    return self

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

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

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

Auf ähnliche Weise können Sie auch eine Funktion binden, indem Sie ihre __get__ Methode manuell ein (nicht wirklich empfohlen, nur zur Veranschaulichung):

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

Man kann dies auch als "selbstgebundene Methode" bezeichnen:

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

Beachten Sie, dass ich keine Argumente angegeben habe und die Funktion die Instanz zurückgibt, die ich gebunden hatte!

Die Funktionen sind Nicht-datenbezogene Deskriptoren !

Einige eingebaute Beispiele für einen Daten-Deskriptor wären property . Vernachlässigung getter , setter y deleter die property Deskriptor ist (von Deskriptor 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 sich um einen Datendeskriptor handelt, wird er immer dann aufgerufen, wenn Sie den "Namen" des property und es delegiert einfach an die Funktionen, die mit @property , @name.setter y @name.deleter (falls vorhanden).

Es gibt mehrere andere Deskriptoren in der Standardbibliothek, zum Beispiel staticmethod , classmethod .

Der Sinn von Deskriptoren ist einfach (obwohl man sie selten braucht): Abstrakter gemeinsamer Code für den Zugriff auf Attribute. property ist eine Abstraktion für den Zugriff auf Instanzvariablen, function bietet eine Abstraktion für Methoden, staticmethod bietet eine Abstraktion für Methoden, die keinen Zugriff auf Instanzen benötigen, und classmethod bietet eine Abstraktion für Methoden, die eher Zugriff auf die Klasse als auf die Instanz benötigen (dies ist etwas vereinfacht).

Ein weiteres Beispiel wäre ein Klasseneigenschaft .

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

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 mit ihm 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 die Verlagerung der Logik in einen gemeinsamen Deskriptor sinnvoll sein könnte, aber man könnte sie auch mit anderen Mitteln lösen (aber vielleicht mit der Wiederholung einiger Codes).

Was ist instance y owner hier? (in __get__ ). Was ist der Zweck dieser Parameter?

Das hängt davon ab, wie man das Attribut nachschlägt. Wenn Sie das Attribut in einer Instanz nachschlagen, dann:

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

Falls Sie das Attribut in der Klasse nachschlagen (vorausgesetzt, der Deskriptor ist in der Klasse definiert):

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

Das dritte Argument ist also notwendig, wenn Sie das Verhalten beim Nachschlagen auf Klassenebene anpassen wollen (weil die instance es None ).

Wie würde ich dieses Beispiel aufrufen/verwenden?

Ihr Beispiel ist im Grunde eine Eigenschaft, die nur Werte zulässt, die in float und der von allen Instanzen der Klasse gemeinsam genutzt wird (und auf die Klasse - obwohl man auf die Klasse nur lesend zugreifen kann, da man sonst die Deskriptorinstanz ersetzen würde):

>>> 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, damit er nicht geteilt wird. In einigen Fällen könnte es jedoch wünschenswert sein, einen Wert zwischen Instanzen zu teilen (obwohl mir im Moment kein Szenario einfällt). Für eine Celsius-Eigenschaft in einer Temperaturklasse macht das jedoch praktisch keinen Sinn... außer vielleicht als rein akademische Übung.

11voto

wllbll Punkte 471

Warum brauche ich die Deskriptorklasse?

Inspiriert durch Fließendes 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 wir ihnen eine negative Zahl zuweisen. Wir können weniger Code schreiben, wenn wir den Deskriptor als Proxy wie folgt 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)

dann definieren Sie 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 Klasse Quantity erweitern, um allgemeinere Validierungen vorzunehmen

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X