798 Stimmen

Verwendung von @property gegenüber Getter und Setter

Welche Vorteile bietet die @property Notation gegenüber der klassischen Getter+Setter-Schreibweise? In welchen spezifischen Fällen/Situationen sollte ein Programmierer die eine Notation der anderen vorziehen?

Mit Eigenschaften:

class MyClass(object):
    @property
    def my_attr(self):
        return self._my_attr

    @my_attr.setter
    def my_attr(self, value):
        self._my_attr = value

Ohne Eigenschaften:

class MyClass(object):
    def get_my_attr(self):
        return self._my_attr

    def set_my_attr(self, value):
        self._my_attr = value

661voto

kindall Punkte 167554

Eigenschaften bevorzugen . Dafür sind sie ja da.

Der Grund dafür ist, dass alle Attribute in Python öffentlich sind. Namen mit einem oder zwei Unterstrichen zu beginnen, ist nur eine Warnung, dass es sich bei dem angegebenen Attribut um ein Implementierungsdetail handelt, das in zukünftigen Versionen des Codes möglicherweise nicht mehr gleich bleibt. Es hindert Sie nicht daran, das Attribut tatsächlich zu erhalten oder zu setzen. Daher ist der Standard-Attributzugriff die normale, pythonische Art, auf Attribute zuzugreifen.

Der Vorteil von Eigenschaften ist, dass sie syntaktisch identisch mit dem Zugriff auf Attribute sind, so dass Sie ohne Änderungen am Client-Code von einem zum anderen wechseln können. Sie könnten sogar eine Version einer Klasse haben, die Eigenschaften verwendet (z. B. für Code-by-Contract oder Debugging), und eine, die dies für die Produktion nicht tut, ohne den Code zu ändern, der sie verwendet. Gleichzeitig müssen Sie nicht für alles Getter und Setter schreiben, nur für den Fall, dass Sie später den Zugriff besser kontrollieren müssen.

176voto

6502 Punkte 108136

In Python verwendet man Getter, Setter oder Eigenschaften nicht einfach nur so zum Spaß. Sie verwenden zunächst nur Attribute und gehen dann später, nur bei Bedarf, zu einer Eigenschaft über, ohne den Code mit Ihren Klassen ändern zu müssen.

Es gibt in der Tat eine Menge Code mit der Endung .py, der Getter und Setter und Vererbung und sinnlose Klassen überall dort verwendet, wo z.B. ein einfaches Tupel ausreichen würde, aber es ist Code von Leuten, die in C++ oder Java schreiben und Python verwenden.

Das ist kein Python-Code.

125voto

Durch die Verwendung von Eigenschaften können Sie mit normalen Attributzugriffen beginnen und dann sie anschließend je nach Bedarf mit Gettern und Settern zu unterstützen .

75voto

mac Punkte 40526

Die kurze Antwort lautet: Eigenschaften gewinnt mit Leichtigkeit . Immer.

Manchmal ist es notwendig, Getter und Setter zu verwenden, aber selbst dann würde ich sie nach außen hin "verstecken". Es gibt viele Möglichkeiten, dies in Python zu tun ( getattr , setattr , __getattribute__ , usw..., aber eine sehr prägnante und saubere ist:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Hier ist ein kurzer Artikel die in das Thema Getter und Setter in Python einführt.

69voto

Adam Donahue Punkte 1576

[ TL;DR? Sie können Springen Sie zum Ende, um ein Codebeispiel zu sehen .]

Ich ziehe es vor, ein anderes Idiom zu verwenden, das ein wenig kompliziert ist, um es einmalig zu verwenden, aber es ist gut, wenn man einen komplexeren Anwendungsfall hat.

Zunächst ein paar Hintergrundinformationen.

Eigenschaften sind insofern nützlich, als sie es uns ermöglichen, sowohl das Setzen als auch das Abrufen von Werten auf programmatische Weise zu handhaben, aber dennoch den Zugriff auf Attribute als Attribute zu ermöglichen. Wir können "gets" in "computations" verwandeln (im Wesentlichen) und wir können "sets" in "events" verwandeln. Nehmen wir also an, wir haben die folgende Klasse, die ich mit Java-ähnlichen Gettern und Settern kodiert habe.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

Sie fragen sich vielleicht, warum ich nicht angerufen habe defaultX y defaultY im Objekt __init__ Methode. Der Grund dafür ist, dass ich für unseren Fall annehmen möchte, dass die someDefaultComputation Methoden geben Werte zurück, die sich im Laufe der Zeit ändern, z. B. einen Zeitstempel, und wenn x (oder y ) nicht gesetzt ist (wobei "nicht gesetzt" in diesem Beispiel "auf Null gesetzt" bedeutet), möchte ich den Wert von x 's (oder y ) Standardberechnung.

Das ist also aus einer Reihe von Gründen, die oben beschrieben wurden, lahm. Ich werde es mit Eigenschaften neu schreiben:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

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

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

Was haben wir gewonnen? Wir haben die Möglichkeit gewonnen, diese Attribute als Attribute zu bezeichnen, auch wenn wir hinter den Kulissen Methoden ausführen.

Die eigentliche Stärke von Eigenschaften liegt natürlich darin, dass wir in der Regel wollen, dass diese Methoden etwas anderes tun, als nur Werte abzurufen und zu setzen (andernfalls wäre es sinnlos, Eigenschaften zu verwenden). Ich habe dies in meinem Getter-Beispiel getan. Wir führen im Grunde einen Funktionskörper aus, um einen Standardwert zu erhalten, wenn der Wert nicht gesetzt ist. Dies ist ein sehr gängiges Muster.

Aber was verlieren wir, und was können wir nicht tun?

Das größte Ärgernis ist meiner Meinung nach, dass man, wenn man einen Getter definiert (wie wir es hier tun), auch einen Setter definieren muss[1] - ein zusätzliches Ärgernis, das den Code unübersichtlich macht.

Ein weiteres Ärgernis ist, dass wir immer noch die x y y Werte in __init__ . (Natürlich können wir sie auch mit setattr() aber das ist mehr zusätzlicher Code).

Drittens können Getter im Gegensatz zu dem Java-ähnlichen Beispiel keine anderen Parameter akzeptieren. Ich höre Sie jetzt schon sagen, na ja, wenn es Parameter annimmt, ist es kein Getter! Im offiziellen Sinne ist das richtig. Aber in einem praktischen Sinn gibt es keinen Grund, warum wir nicht in der Lage sein sollten, ein benanntes Attribut zu parametrisieren -- wie x -- und setzen seinen Wert für einige spezifische Parameter.

Es wäre schön, wenn wir so etwas tun könnten:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

zum Beispiel. Das Naheliegendste, was wir tun können, ist, die Zuweisung zu überschreiben, um eine spezielle Semantik zu implizieren:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

und natürlich sicherstellen, dass unser Setter weiß, wie man die ersten drei Werte als Schlüssel für ein Wörterbuch extrahiert und seinen Wert auf eine Zahl oder etwas anderes setzt.

Aber selbst wenn wir das täten, könnten wir es immer noch nicht mit Eigenschaften unterstützen, weil es keine Möglichkeit gibt, den Wert zu erhalten, weil wir dem Getter überhaupt keine Parameter übergeben können. Wir mussten also alles zurückgeben, was zu einer Asymmetrie führte.

Die Java-Stil Getter/Setter lässt uns dies zu behandeln, aber wir sind zurück zu benötigen Getter/Setter.

Meiner Meinung nach brauchen wir etwas, das die folgenden Anforderungen erfüllt:

  • Die Benutzer definieren nur eine Methode für ein bestimmtes Attribut und können dort angeben ob das Attribut schreibgeschützt oder schreibgeschützt ist. Eigenschaften bestehen diesen Test nicht wenn das Attribut beschreibbar ist.

  • Es besteht keine Notwendigkeit für den Benutzer, eine zusätzliche Variable zu definieren, die der Funktion zugrundeliegt, so dass wir die __init__ o setattr im Code. Die Variable existiert nur durch die Tatsache, dass wir dieses neue Attribut erstellt haben.

  • Jeder Standardcode für das Attribut wird im Methodenrumpf selbst ausgeführt.

  • Wir können das Attribut als Attribut festlegen und es als Attribut referenzieren.

  • Wir können das Attribut parametrisieren.

Was den Code betrifft, wollen wir eine Möglichkeit zum Schreiben:

def x(self, *args):
    return defaultX()

und in der Lage sein, dies zu tun:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

und so weiter.

Wir wollen auch eine Möglichkeit, dies für den speziellen Fall eines parametrisierbaren Attributs zu tun, aber dennoch den Standard-Zuweisungsfall zu ermöglichen. Sie werden sehen, wie ich das unten angegangen bin.

Nun zur Sache (juhu! zur Sache!). Die Lösung, die ich mir dafür ausgedacht habe, lautet wie folgt.

Wir erstellen ein neues Objekt, das den Begriff der Eigenschaft ersetzt. Das Objekt ist dazu gedacht, den Wert einer Variablen zu speichern, die ihm zugewiesen wurde, aber auch eine Handhabe für Code zu haben, der weiß, wie man einen Standardwert berechnet. Seine Aufgabe ist es, die Menge zu speichern value oder um die method wenn dieser Wert nicht gesetzt ist.

Nennen wir es eine UberProperty .

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

Ich nehme an method hier ist eine Klassenmethode, value ist der Wert der UberProperty und ich habe hinzugefügt isSet porque None kann ein echter Wert sein, und dies ermöglicht uns eine saubere Art zu erklären, dass es wirklich "keinen Wert" gibt. Eine andere Möglichkeit ist eine Art Sentinel.

Damit haben wir im Grunde ein Objekt, das tun kann, was wir wollen, aber wie können wir es in unsere Klasse einbauen? Nun, Eigenschaften verwenden Dekoratoren; warum können wir das nicht? Schauen wir uns an, wie es aussehen könnte (von hier an werde ich nur noch ein einziges 'Attribut' verwenden, x ).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

Das funktioniert natürlich noch nicht wirklich. Wir müssen implementieren uberProperty und stellen Sie sicher, dass es sowohl gets als auch sets verarbeitet.

Beginnen wir mit "gets".

Mein erster Versuch war, einfach ein neues UberProperty-Objekt zu erstellen und es zurückzugeben:

def uberProperty(f):
    return UberProperty(f)

Ich habe natürlich schnell festgestellt, dass das nicht funktioniert: Python bindet den Callable nie an das Objekt und ich brauche das Objekt, um die Funktion aufzurufen. Auch das Erstellen des Dekorators in der Klasse funktioniert nicht, denn obwohl wir jetzt die Klasse haben, haben wir immer noch kein Objekt, mit dem wir arbeiten können.

Wir müssen hier also mehr tun können. Wir wissen, dass eine Methode nur ein einziges Mal dargestellt werden muss, also behalten wir unseren Dekorator bei, ändern aber UberProperty um nur die method Hinweis:

class UberProperty(object):

    def __init__(self, method):
        self.method = method

Sie ist auch nicht aufrufbar, so dass im Moment nichts funktioniert.

Wie können wir das Bild vervollständigen? Nun, was haben wir am Ende, wenn wir die Beispielklasse mit unserem neuen Dekorator erstellen:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

in beiden Fällen erhalten wir die UberProperty die natürlich nicht aufrufbar ist, so dass dies nicht von großem Nutzen ist.

Was wir brauchen, ist ein Weg, um dynamisch die UberProperty Instanz, die vom Dekorator nach der Erstellung der Klasse erzeugt wurde, in ein Objekt der Klasse umzuwandeln, bevor dieses Objekt an den Benutzer zur Verwendung zurückgegeben wird. Ähm, ja, das ist ein __init__ anrufen, Kumpel.

Lassen Sie uns zuerst aufschreiben, was unser Suchergebnis sein soll. Wir binden eine UberProperty zu einer Instanz, so dass es naheliegend wäre, eine BoundUberProperty zurückzugeben. Hier werden wir tatsächlich den Zustand für die x Attribut.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Nun zur Darstellung: Wie bekommt man diese auf ein Objekt? Es gibt einige Ansätze, aber der am einfachsten zu erklärende verwendet einfach die __init__ Methode, um diese Zuordnung vorzunehmen. Zu der Zeit __init__ aufgerufen wird, sind unsere Dekorateure gelaufen, wir müssen also nur die __dict__ und aktualisieren Sie alle Attribute, bei denen der Wert des Attributs vom Typ UberProperty .

Nun, Uber-Properties sind cool und wir werden sie wahrscheinlich oft verwenden wollen, daher ist es sinnvoll, eine Basisklasse zu erstellen, die dies für alle Unterklassen tut. Ich denke, Sie wissen, wie die Basisklasse genannt werden soll.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

Wir fügen dies hinzu, ändern unser Beispiel und erben von UberObject und ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

Nach der Änderung von x zu sein:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

Wir können einen einfachen Test durchführen:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

Und wir erhalten das gewünschte Ergebnis:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Oh je, ich arbeite lange.)

Beachten Sie, dass ich getValue , setValue y clearValue hier. Das liegt daran, dass ich noch keine Möglichkeit gefunden habe, diese automatisch zurückzusenden.

Aber ich denke, das ist ein guter Zeitpunkt, um aufzuhören, denn ich werde müde. Sie können auch sehen, dass die Kernfunktionalität, die wir wollten, vorhanden ist; der Rest ist Augenwischerei. Der Rest ist Schaufensterdekoration, aber das kann warten, bis ich die Gelegenheit habe, den Beitrag zu aktualisieren.

Ich werde das Beispiel im nächsten Beitrag abschließen, indem ich auf diese Dinge eingehe:

  • Wir müssen sicherstellen, dass die UberObjects __init__ wird immer von Unterklassen aufgerufen.

    • Also zwingen wir entweder, dass sie irgendwo aufgerufen wird, oder wir verhindern, dass sie implementiert wird.
    • Wir werden sehen, wie man das mit einer Metaklasse macht.
  • Wir müssen sicherstellen, dass wir den häufigen Fall behandeln, dass jemand ein "Alias" für eine Funktion auf etwas anderes "aliasiert", wie zum Beispiel:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Wir brauchen e.x zur Rückkehr e.x.getValue() standardmäßig.

    • Wir werden sehen, dass dies ein Bereich ist, in dem das Modell versagt.
    • Es stellt sich heraus, dass wir immer einen Funktionsaufruf verwenden müssen, um den Wert zu erhalten.
    • Aber wir können es wie einen normalen Funktionsaufruf aussehen lassen und vermeiden, dass wir die e.x.getValue() . (Dies zu tun, ist offensichtlich, falls Sie es nicht schon herausgefunden haben).
  • Wir müssen die Einstellung unterstützen e.x directly wie in e.x = <newvalue> . Wir können dies auch in der übergeordneten Klasse tun, aber wir müssen unsere __init__ Code, um damit umzugehen.

  • Schließlich werden wir parametrisierte Attribute hinzufügen. Es sollte ziemlich offensichtlich sein, wie wir das machen.

Hier ist der Code, wie er bis jetzt existiert:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] Ob dies immer noch der Fall ist, kann ich nicht beurteilen.

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