1331 Stimmen

Wie funktioniert der @property-Dekorator in Python?

Ich würde gerne verstehen, wie die eingebaute Funktion property funktioniert. Was mich verwirrt, ist dass property auch als Dekorator verwendet werden kann, aber nur Argumente akzeptiert, wenn es als eingebaute Funktion verwendet wird und nicht wenn es als Dekorator verwendet wird.

Dieses Beispiel stammt aus der Dokumentation:

class C:
    def __init__(self):
        self._x = None

    def getx(self):
        return self._x
    def setx(self, value):
        self._x = value
    def delx(self):
        del self._x
    x = property(getx, setx, delx, "Ich bin die 'x' Eigenschaft.")

Die Argumente von property sind getx, setx, delx und eine Dokumentationszeichenkette.

In dem folgenden Code wird property als Dekorator verwendet. Das Objekt davon ist die Funktion x, aber im obigen Code gibt es keinen Platz für eine Objektfunktion in den Argumenten.

class C:
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """Ich bin die 'x' Eigenschaft."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x

Wie werden die Dekoratoren x.setter und x.deleter in diesem Fall erstellt?

1267voto

Martijn Pieters Punkte 953257

Die Funktion property() gibt ein spezielles Deskriptor-Objekt zurück:

>>> property()

Es handelt sich bei diesem Objekt um ein Objekt, das zusätzliche Methoden hat:

>>> property().getter

>>> property().setter

>>> property().deleter

Diese fungieren auch als Dekorateure. Sie geben ein neues Property-Objekt zurück:

>>> property().getter(None)

das eine Kopie des alten Objekts ist, jedoch mit einer der Funktionen ersetzt.

Denken Sie daran, dass die Syntax @decorator nur syntaktischer Zucker ist; die Syntax:

@property
def foo(self): return self._foo

bedeutet wirklich das Gleiche wie

def foo(self): return self._foo
foo = property(foo)

also wird die Funktion foo durch property(foo) ersetzt, was wir oben als spezielles Objekt gesehen haben. Wenn Sie dann @foo.setter() verwenden, rufen Sie die Methode property().setter auf, die eine neue Kopie des Property-Objekts zurückgibt, diesmal jedoch mit der Setter-Funktion ersetzt durch die dekorierte Methode.

Die folgende Sequenz erstellt ebenfalls ein vollwertiges Property, indem diese Dekoratormethoden verwendet werden.

Zuerst erstellen wir einige Funktionen und ein property-Objekt nur mit einem Getter:

>>> def getter(self): print('Get!')
... 
>>> def setter(self, value): print('Setze auf {!r}!'.format(value))
... 
>>> def deleter(self): print('Löschen!')
... 
>>> prop = property(getter)
>>> prop.fget is getter
True
>>> prop.fset is None
True
>>> prop.fdel is None
True

Dann verwenden wir die Methode .setter(), um einen Setter hinzuzufügen:

>>> prop = prop.setter(setter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is None
True

Zuletzt fügen wir mit der Methode .deleter() einen Löscher hinzu:

>>> prop = prop.deleter(deleter)
>>> prop.fget is getter
True
>>> prop.fset is setter
True
>>> prop.fdel is deleter
True

Zu guter Letzt fungiert das property-Objekt als Deskriptor-Objekt, sodass es Methoden wie .__get__(), .__set__() und .__delete__() hat, um in das Abrufen, Setzen und Löschen von Instanzattributen einzugreifen:

>>> class Foo: pass
... 
>>> prop.__get__(Foo(), Foo)
Get!
>>> prop.__set__(Foo(), 'bar')
Setze auf 'bar'!
>>> prop.__delete__(Foo())
Löschen!

Der Descriptor Howto enthält eine reine Python-Beispielimplementierung des Typs property():

class Property:
    "Emuliere 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("nicht lesbares Attribut")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("Attribut kann nicht gesetzt werden")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("Attribut kann nicht gelöscht werden")
        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__)

346voto

J0HN Punkte 25228

Die Dokumentation besagt, dass es nur ein Shortcut zur Erstellung von schreibgeschützten Eigenschaften ist. Also

@property
def x(self):
    return self._x

ist äquivalent zu

def getx(self):
    return self._x
x = property(getx)

178voto

Alex Punkte 9996

Hier ist ein minimales Beispiel, wie @property implementiert werden kann:

class Thing:
    def __init__(self, my_word):
        self._word = my_word 
    @property
    def word(self):
        return self._word

>>> print( Thing('ok').word )
'ok'

Andernfalls bleibt word eine Methode und keine Eigenschaft.

class Thing:
    def __init__(self, my_word):
        self._word = my_word
    def word(self):
        return self._word

>>> print( Thing('ok').word() )
'ok'

112voto

Cleb Punkte 22735

Im folgenden finden Sie ein weiteres Beispiel dafür, wie @property helfen kann, wenn Code refakturiert werden muss, das von hier stammt (ich fasse es nur kurz zusammen):

Stellen Sie sich vor, Sie haben eine Klasse Money wie diese erstellt:

class Money:
    def __init__(self, dollars, cents):
        self.dollars = dollars
        self.cents = cents

und ein Benutzer erstellt eine Bibliothek, die von dieser Klasse abhängt, in der er/sie z.B. verwendet

money = Money(27, 12)

print("Ich habe {} Dollar und {} Cent.".format(money.dollars, money.cents))
# gibt aus Ich habe 27 Dollar und 12 Cent.

Nehmen wir nun an, Sie entscheiden sich dazu, Ihre Money-Klasse zu ändern und die Attribute dollars und cents loszuwerden, sondern entscheiden stattdessen, nur die Gesamtmenge an Cents zu verfolgen:

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

Wenn der oben erwähnte Benutzer nun versucht, seine/ihre Bibliothek wie zuvor auszuführen

money = Money(27, 12)

print("Ich habe {} Dollar und {} Cent.".format(money.dollars, money.cents))

es wird zu einem Fehler führen

AttributeError: 'Money' object hat kein Attribut 'dollars'

Das bedeutet, dass nun jeder, der sich auf Ihre ursprüngliche Money-Klasse verlässt, alle Codezeilen ändern müsste, in denen dollars und cents verwendet werden, was sehr schmerzhaft sein kann... Also, wie könnte dies vermieden werden? Durch Verwendung von @property!

So funktioniert es:

class Money:
    def __init__(self, dollars, cents):
        self.total_cents = dollars * 100 + cents

    # Getter und Setter für Dollar...
    @property
    def dollars(self):
        return self.total_cents // 100

    @dollars.setter
    def dollars(self, new_dollars):
        self.total_cents = 100 * new_dollars + self.cents

    # Und der Getter und Setter für Cents.
    @property
    def cents(self):
        return self.total_cents % 100

    @cents.setter
    def cents(self, new_cents):
        self.total_cents = 100 * self.dollars + new_cents

wenn wir jetzt aus unserer Bibliothek aufrufen

money = Money(27, 12)

print("Ich habe {} Dollar und {} Cent.".format(money.dollars, money.cents))
# gibt aus Ich habe 27 Dollar und 12 Cent.

wird es wie erwartet funktionieren und wir mussten keinen einzigen Code in unserer Bibliothek ändern! Tatsächlich müssten wir nicht einmal wissen, dass sich die von uns abhängige Bibliothek geändert hat.

Auch der setter funktioniert gut:

money.dollars += 2
print("Ich habe {} Dollar und {} Cent.".format(money.dollars, money.cents))
# gibt aus Ich habe 29 Dollar und 12 Cent.

money.cents += 10
print("Ich habe {} Dollar und {} Cent.".format(money.dollars, money.cents))
# gibt aus Ich habe 29 Dollar und 22 Cent.

Sie können @property auch in abstrakten Klassen verwenden; ich gebe ein minimales Beispiel hier.

96voto

glglgl Punkte 84928

Der erste Teil ist einfach:

@property
def x(self): ...

ist dasselbe wie

def x(self): ...
x = property(x)
  • was wiederum die vereinfachte Syntax für die Erstellung einer property nur mit einem Getter ist.

Der nächste Schritt wäre, diese Property mit einem Setter und einem Deleter zu erweitern. Und das geschieht mit den entsprechenden Methoden:

@x.setter
def x(self, value): ...

gibt eine neue Property zurück, die alles von der alten x sowie den angegebenen Setter erbt.

x.deleter funktioniert auf die gleiche Weise.

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