Verwenden von @property versus Getter und Setter

727

Hier ist eine reine Python-spezifische Designfrage:

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

    def set_my_attr(self, value):
        ...

und

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

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

Mit Python können wir es so oder so machen. Wenn Sie ein Python-Programm entwerfen würden, welchen Ansatz würden Sie verwenden und warum?

Zaur Nasibov
quelle

Antworten:

613

Lieber Eigenschaften . Dafür sind sie da.

Der Grund ist, dass alle Attribute in Python öffentlich sind. Das Starten von Namen mit einem oder zwei Unterstrichen ist nur eine Warnung, dass das angegebene Attribut ein Implementierungsdetail ist, das in zukünftigen Versionen des Codes möglicherweise nicht gleich bleibt. Es hindert Sie nicht daran, dieses Attribut tatsächlich zu erhalten oder festzulegen. Daher ist der Standardzugriff auf Attribute die normale pythonische Methode, um auf Attribute zuzugreifen.

Der Vorteil von Eigenschaften besteht darin, dass sie syntaktisch mit dem Attributzugriff identisch sind, sodass Sie von einem zum anderen wechseln können, ohne den Clientcode zu ändern. Sie könnten sogar eine Version einer Klasse haben, die Eigenschaften verwendet (z. B. für Code-by-Contract oder Debugging), und eine, die nicht für die Produktion verwendet wird, 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 den Zugriff später besser steuern müssen.

irgendwie
quelle
90
Attributnamen mit einem doppelten Unterstrich werden speziell von Python behandelt. Es ist nicht nur eine bloße Konvention. Siehe docs.python.org/py3k/tutorial/classes.html#private-variables
6502
63
Sie werden unterschiedlich behandelt, aber das hindert Sie nicht daran, auf sie zuzugreifen. PS: AD 30 C0
bitte
4
und weil "@" Zeichen im Python-Code hässlich sind und die Dereferenzierung von @decorators das gleiche Gefühl wie Spaghetti-Code vermittelt.
Berry Tsakala
18
Ich stimme nicht zu Wie ist strukturierter Code gleich Spaghetti-Code? Python ist eine schöne Sprache. Wäre aber noch besser mit besserer Unterstützung für einfache Dinge wie richtige Kapselung und strukturierte Klassen.
69
Obwohl ich in den meisten Fällen zustimme, sollten Sie vorsichtig sein, wenn Sie langsame Methoden hinter einem @ property-Dekorateur verstecken. Der Benutzer Ihrer API erwartet, dass der Eigenschaftszugriff wie ein variabler Zugriff funktioniert. Wenn Sie zu weit von dieser Erwartung entfernt sind, kann die Verwendung Ihrer API unangenehm werden.
Defrex
153

In Python verwenden Sie Getter oder Setter oder Eigenschaften nicht nur zum Spaß. Sie verwenden zuerst nur Attribute und migrieren später, nur bei Bedarf, zu einer Eigenschaft, ohne den Code mithilfe Ihrer Klassen ändern zu müssen.

Es gibt in der Tat viel Code mit der Erweiterung .py, der Getter und Setter sowie Vererbung und sinnlose Klassen überall dort verwendet, wo beispielsweise ein einfaches Tupel ausreichen würde, aber es ist Code von Leuten, die mit Python in C ++ oder Java schreiben.

Das ist kein Python-Code.

6502
quelle
46
@ 6502, als Sie sagten "[…] sinnlose Klassen überall dort, wo z. B. ein einfaches Tupel ausreichen würde": Der Vorteil einer Klasse gegenüber einem Tupel besteht darin, dass eine Klasseninstanz explizite Namen für den Zugriff auf ihre Teile bereitstellt, während ein Tupel dies nicht tut . Namen sind besser lesbar und vermeiden Fehler als Tupel-Subskriptionen, insbesondere wenn diese außerhalb des aktuellen Moduls übergeben werden sollen.
Hibou57
15
@ Hibou57: Ich sage nicht, dass Unterricht nutzlos ist. Aber manchmal ist ein Tupel mehr als genug. Das Problem ist jedoch, dass derjenige, der beispielsweise aus Java oder C ++ stammt, keine andere Wahl hat, als Klassen für alles zu erstellen, da andere Möglichkeiten in diesen Sprachen nur ärgerlich sind. Ein weiteres typisches Symptom der Java / C ++ - Programmierung mit Python ist das Erstellen abstrakter Klassen und komplexer Klassenhierarchien ohne Grund, wo Sie in Python dank Ententypisierung nur unabhängige Klassen verwenden können.
6502
39
@ Hibou57 dafür können Sie auch namedtuple verwenden: teigellmann.com/PyMOTW/collections/namedtuple.html
hugo24
5
@JonathonReinhart: es IST seit 2.6 in der Standardbibliothek ... siehe docs.python.org/2/library/collections.html
6502
1
Wenn Sie "eventuell bei Bedarf zu einer Eigenschaft migrieren", ist es sehr wahrscheinlich, dass Sie Code mithilfe Ihrer Klassen beschädigen. Eigenschaften führen häufig Einschränkungen ein - Code, der diese Einschränkungen nicht erwartet hat, wird sofort nach der Einführung aufgehoben.
Yaccob
118

Wenn Sie Eigenschaften verwenden, können Sie mit normalen Attributzugriffen beginnen und diese anschließend bei Bedarf mit Getter und Setter sichern .

Ignacio Vazquez-Abrams
quelle
3
@ GregKrsak Es scheint seltsam, weil es ist. Das "zustimmende Erwachsenen-Ding" war ein Python-Mem, bevor Eigenschaften hinzugefügt wurden. Es war die Aktienantwort auf Leute, die sich über das Fehlen von Zugriffsmodifikatoren beschweren würden. Wenn Eigenschaften hinzugefügt wurden, wird plötzlich eine Einkapselung wünschenswert. Dasselbe geschah mit abstrakten Basisklassen. "Python war immer im Krieg mit Kapselungsbrüchen. Freiheit ist Sklaverei. Lambdas sollten nur in eine Zeile passen."
Johncip
71

Die kurze Antwort lautet: Eigenschaften gewinnen zweifellos . Immer.

Es besteht manchmal ein Bedarf an Gettern und Setzern, aber selbst dann würde ich sie nach außen "verstecken". Es gibt viele Möglichkeiten , dies in Python zu tun ( getattr, setattr, __getattribute__, etc ..., aber ein sehr präzise und sauber 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 , der das Thema Getter und Setter in Python einführt.

Mac
quelle
1
@BasicWolf - Ich dachte, es wäre implizit klar, dass ich auf der Grundstücksseite des Zauns bin! :) Aber ich füge meiner Antwort einen Absatz hinzu, um das zu verdeutlichen.
Mac
9
TIPP: Das Wort "immer" ist ein Hinweis darauf, dass der Autor versucht, Sie mit einer Behauptung zu überzeugen, nicht mit einem Argument. So ist das Vorhandensein von Fettdruck Schrift. (Ich meine, wenn Sie stattdessen CAPS sehen, dann - whoa - muss es richtig sein.) Schauen Sie, die "Eigenschaft" -Funktion unterscheidet sich zufällig von Java (Pythons De-facto-Nemesis aus irgendeinem Grund) und daher Pythons Community-Groupthink erklärt es für besser. In Wirklichkeit verstoßen Eigenschaften gegen die Regel "Explizit ist besser als implizit", aber niemand möchte dies zugeben. Es hat es in die Sprache geschafft, und jetzt wird es über ein tautologisches Argument als "Pythonic" deklariert.
Stuart Berg
3
Keine Gefühle tun weh. :-P Ich möchte nur darauf hinweisen, dass "Pythonic" -Konventionen in diesem Fall inkonsistent sind: "Explizit ist besser als implizit" steht in direktem Konflikt mit der Verwendung von a property. (Es sieht aus wie eine einfache Aufgabe, nennt aber eine Funktion.) Daher ist "Pythonic" im Wesentlichen ein bedeutungsloser Begriff, außer durch die tautologische Definition: "Pythonic-Konventionen sind Dinge, die wir als Pythonic definiert haben."
Stuart Berg
1
Die Idee , eine Reihe von Konventionen zu haben, die einem Thema folgen, ist großartig . Wenn es eine solche Reihe von Konventionen gäbe, könnten Sie sie als eine Reihe von Axiomen verwenden, um Ihr Denken zu leiten, und nicht nur als eine lange Checkliste mit Tricks, die Sie sich merken sollten, was wesentlich weniger nützlich ist. Axiome könnten zur Extrapolation verwendet werden und Ihnen helfen, Probleme anzugehen , die noch niemand gesehen hat. Es ist eine Schande, dass das propertyFeature die Idee der pythonischen Axiome nahezu wertlos zu machen droht. Wir haben also nur noch eine Checkliste.
Stuart Berg
1
Ich stimme nicht zu Ich bevorzuge Eigenschaften in den meisten Situationen, aber wenn Sie betonen möchten, dass das Festlegen von etwas anderen Nebenwirkungen als das Ändern des selfObjekts hat , können explizite Setter hilfreich sein. Es user.email = "..."sieht beispielsweise nicht so aus, als könnte es eine Ausnahme auslösen, da es so aussieht, als würde nur ein Attribut festgelegt, während user.set_email("...")klar ist, dass es Nebenwirkungen wie Ausnahmen geben kann.
bluenote10
65

[ TL; DR? Sie können bis zum Ende springen, um ein Codebeispiel zu erhalten .]

Eigentlich bevorzuge ich die Verwendung einer anderen Redewendung, die für die einmalige Verwendung etwas aufwendig ist, aber schön ist, wenn Sie einen komplexeren Anwendungsfall haben.

Zuerst ein bisschen Hintergrund.

Eigenschaften sind insofern nützlich, als sie es uns ermöglichen, sowohl das Einstellen als auch das Abrufen von Werten programmgesteuert zu behandeln, aber dennoch den Zugriff auf Attribute als Attribute zu ermöglichen. Wir können "Gets" (im Wesentlichen) in "Berechnungen" und "Sets" in "Ereignisse" verwandeln. Nehmen wir also an, wir haben die folgende Klasse, die ich mit Java-ähnlichen Gettern und Setzern codiert 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 aufgerufen habe defaultXund defaultYin der __init__Methode des Objekts . Der Grund ist, dass ich für unseren Fall annehmen möchte, dass die someDefaultComputationMethoden Werte zurückgeben, die sich über die Zeit ändern, beispielsweise einen Zeitstempel, und wann immer x(oder y) nicht gesetzt ist (wobei für den Zweck dieses Beispiels "nicht gesetzt" "gesetzt" bedeutet to None ") Ich möchte den Wert der Standardberechnung von x's (oder y' s).

Dies ist aus einer Reihe von Gründen, die oben beschrieben wurden, lahm. Ich werde es mit Eigenschaften umschreiben:

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 erhalten, diese Attribute als Attribute zu bezeichnen, obwohl wir hinter den Kulissen Methoden ausführen.

Die wahre Stärke von Eigenschaften besteht natürlich darin, dass diese Methoden im Allgemeinen nicht nur Werte abrufen und festlegen sollen (andernfalls macht es keinen Sinn, Eigenschaften zu verwenden). Ich habe das in meinem Getter-Beispiel gemacht. Grundsätzlich führen wir einen Funktionskörper aus, um einen Standardwert zu ermitteln, wenn der Wert nicht festgelegt ist. Dies ist ein sehr verbreitetes Muster.

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

Meiner Ansicht nach ist der größte Ärger, dass Sie, wenn Sie einen Getter definieren (wie wir es hier tun), auch einen Setter definieren müssen. [1] Das ist zusätzliches Rauschen, das den Code überfüllt.

Ein weiterer Ärger ist, dass wir die xund y-Werte noch initialisieren müssen __init__. (Nun, natürlich könnten wir sie mit hinzufügen, setattr()aber das ist mehr zusätzlicher Code.)

Drittens können Getter im Gegensatz zum Java-ähnlichen Beispiel keine anderen Parameter akzeptieren. Jetzt kann ich Sie schon sagen hören, na ja, wenn es Parameter nimmt, ist es kein Getter! Im offiziellen Sinne ist das wahr. In praktischer Hinsicht gibt es jedoch keinen Grund, warum wir nicht in der Lage sein sollten, ein benanntes Attribut wie zu parametrisieren xund seinen Wert für bestimmte Parameter festzulegen.

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

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

zum Beispiel. Das Beste, was wir bekommen 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 setzt.

Aber selbst wenn wir das tun würden, könnten wir es nicht mit Eigenschaften unterstützen, da es keine Möglichkeit gibt, den Wert abzurufen, da wir überhaupt keine Parameter an den Getter übergeben können. Wir mussten also alles zurückgeben und eine Asymmetrie einführen.

Der Java-artige Getter / Setter lässt uns damit umgehen, aber wir brauchen wieder Getter / Setter.

In meinen Augen wollen wir wirklich etwas, das die folgenden Anforderungen erfüllt:

  • 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 schlagen diesen Test fehl, wenn das Attribut beschreibbar ist.

  • Der Benutzer muss keine zusätzliche Variable definieren, die der Funktion zugrunde liegt, daher benötigen wir das __init__oder setattrim Code nicht. Die Variable existiert nur dadurch, dass wir dieses Attribut im neuen Stil erstellt haben.

  • Jeder Standardcode für das Attribut wird im Methodenkörper selbst ausgeführt.

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

  • Wir können das Attribut parametrisieren.

In Bezug auf Code wollen wir eine Möglichkeit zu schreiben:

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

und dann in der Lage sein:

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 möchten dies auch für den Sonderfall eines parametrierbaren Attributs tun, aber dennoch zulassen, dass der Standard-Zuweisungsfall funktioniert. Sie werden unten sehen, wie ich das angegangen bin.

Nun zum Punkt (yay! Der Punkt!). Die Lösung, die ich dafür gefunden habe, ist wie folgt.

Wir erstellen ein neues Objekt, um den Begriff einer Eigenschaft zu ersetzen. Das Objekt soll den Wert einer darauf gesetzten Variablen speichern, verwaltet jedoch auch ein Handle für Code, der weiß, wie ein Standard berechnet wird. Seine Aufgabe ist es, die Menge zu speichern valueoder die auszuführen, methodwenn dieser Wert nicht gesetzt ist.

Nennen wir es ein 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, methodhier ist eine Klassenmethode, valueist der Wert von UberProperty, und ich habe hinzugefügt, isSetweil es sich Nonemöglicherweise um einen echten Wert handelt, und dies ermöglicht uns eine saubere Möglichkeit, zu erklären, dass es wirklich "keinen Wert" gibt. Ein anderer Weg ist eine Art Wachposten.

Dies gibt uns im Grunde ein Objekt, das tun kann, was wir wollen, aber wie setzen wir es tatsächlich in unsere Klasse ein? Nun, Eigenschaften verwenden Dekorateure; warum können wir nicht Mal sehen, wie es aussehen könnte (von jetzt 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. Wir müssen implementieren uberPropertyund sicherstellen, dass es sowohl Gets als auch Sets verarbeitet.

Beginnen wir mit bekommen.

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

def uberProperty(f):
    return UberProperty(f)

Ich stellte natürlich schnell fest, dass dies nicht funktioniert: Python bindet den Aufrufbaren niemals an das Objekt und ich benötige das Objekt, um die Funktion aufzurufen. Selbst das Erstellen des Dekorators in der Klasse funktioniert nicht, da wir jetzt zwar die Klasse haben, aber immer noch kein Objekt haben, mit dem wir arbeiten können.

Wir müssen hier also mehr tun können. Wir wissen, dass eine Methode nur einmal dargestellt werden muss. Lassen Sie uns also unseren Dekorateur behalten, aber ändern Sie sie, UberPropertyum nur die methodReferenz zu speichern :

class UberProperty(object):

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

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

Wie vervollständigen wir das Bild? 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 das zurück, UberPropertywas natürlich nicht aufrufbar ist, daher ist dies nicht sehr nützlich.

Was wir brauchen, ist eine Möglichkeit, UberPropertydie vom Dekorateur erstellte Instanz dynamisch zu binden, nachdem die Klasse an ein Objekt der Klasse erstellt wurde, bevor dieses Objekt zur Verwendung an diesen Benutzer zurückgegeben wurde. Ähm, ja, das ist ein __init__Anruf, Alter.

Schreiben wir auf, was unser Suchergebnis zuerst sein soll. Wir binden eine UberPropertyan eine Instanz, daher ist es naheliegend, eine BoundUberProperty zurückzugeben. Hier behalten wir den Status für das xAttribut bei.

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

Jetzt haben wir die Darstellung; Wie bringen Sie diese auf ein Objekt? Es gibt einige Ansätze, aber der am einfachsten zu erklärende verwendet nur die __init__Methode, um dieses Mapping durchzuführen. Zu dem Zeitpunkt __init__, an dem unsere Dekorateure aufgerufen sind, müssen sie nur noch die Objekte durchsehen __dict__und alle Attribute aktualisieren, bei denen der Wert des Attributs vom Typ ist UberProperty.

Jetzt sind Uber-Eigenschaften cool und wir werden sie wahrscheinlich häufig verwenden wollen. Daher ist es sinnvoll, nur eine Basisklasse zu erstellen, die dies für alle Unterklassen tut. Ich denke, Sie wissen, wie die Basisklasse heißen wird.

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, um von zu erben UberObject, und ...

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

Nach dem Ändern xzu 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 bekommen die Ausgabe, die wir wollten:

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

(Gee, ich arbeite spät.)

Beachten Sie, dass ich verwendet habe getValue, setValueund clearValuehier. Dies liegt daran, dass ich noch nicht die Mittel verknüpft habe, um diese automatisch zurückzugeben.

Aber ich denke, dies ist ein guter Ort, um vorerst anzuhalten, weil ich müde werde. Sie können auch sehen, dass die von uns gewünschte Kernfunktionalität vorhanden ist. Der Rest ist Schaufensterdekoration. Wichtige Usability-Fensterdekoration, aber das kann warten, bis ich eine Änderung habe, um den Beitrag zu aktualisieren.

Ich werde das Beispiel im nächsten Beitrag beenden, indem ich folgende Dinge anspreche:

  • Wir müssen sicherstellen, dass UberObjects __init__immer von Unterklassen aufgerufen wird.

    • Also erzwingen wir entweder, dass es irgendwo aufgerufen wird, oder wir verhindern, dass es implementiert wird.
    • Wir werden sehen, wie das mit einer Metaklasse geht.
  • Wir müssen sicherstellen, dass wir den allgemeinen Fall behandeln, in dem jemand eine Funktion auf etwas anderes 'aliasisiert', wie zum Beispiel:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
  • Wir müssen e.xzurück in e.x.getValue()der Standardeinstellung.

    • Was wir tatsächlich sehen werden, ist, 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, es verwenden zu müssen e.x.getValue(). (Dies zu tun ist offensichtlich, wenn Sie es noch nicht behoben 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 unseren __init__Code aktualisieren , um damit umzugehen.

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

Hier ist der Code, wie er bisher 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] Ich bin möglicherweise im Rückstand, ob dies noch der Fall ist.

Adam Donahue
quelle
53
Ja, das ist 'tldr'. Können Sie bitte zusammenfassen, was Sie hier versuchen?
Poolie
9
@ Adam return self.x or self.defaultX()das ist gefährlicher Code. Was passiert wann self.x == 0?
Kelly Thomas
Zu Ihrer Information, Sie können es so machen, dass Sie den Getter irgendwie parametrisieren können. Dazu müsste die Variable zu einer benutzerdefinierten Klasse gemacht werden, deren __getitem__Methode Sie überschrieben haben . Es wäre allerdings seltsam, da Sie dann völlig andere Standard-Python haben würden.
wird
2
@ KellyThomas Ich versuche nur, das Beispiel einfach zu halten. Um es richtig zu machen, müssen Sie den x dict- Eintrag insgesamt erstellen und löschen , da möglicherweise sogar ein None-Wert speziell festgelegt wurde. Aber ja, Sie haben absolut Recht, dies ist etwas, das Sie in einem Anwendungsfall für die Produktion berücksichtigen müssten.
Adam Donahue
Mit Java-ähnlichen Gettern können Sie genau die gleiche Berechnung durchführen, nicht wahr?
Qed
26

Ich denke, beide haben ihren Platz. Ein Problem bei der Verwendung @propertyist, dass es schwierig ist, das Verhalten von Gettern oder Setzern in Unterklassen mithilfe von Standardklassenmechanismen zu erweitern. Das Problem ist, dass die tatsächlichen Getter / Setter-Funktionen in der Eigenschaft versteckt sind.

Sie können die Funktionen tatsächlich erreichen, z. B. mit

class C(object):
    _p = 1
    @property
    def p(self):
        return self._p
    @p.setter
    def p(self, val):
        self._p = val

Sie können als C.p.fgetund auf die Getter- und Setter-Funktionen zugreifen C.p.fset, aber Sie können die normalen Methodenvererbungsfunktionen (z. B. Super) nicht einfach verwenden, um sie zu erweitern. Nach einigem Graben in die Feinheiten von super, man kann in der Tat auf diese Weise verwendet Super:

# Using super():
class D(C):
    # Cannot use super(D,D) here to define the property
    # since D is not yet defined in this scope.
    @property
    def p(self):
        return super(D,D).p.fget(self)

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for D'
        super(D,D).p.fset(self, val)

# Using a direct reference to C
class E(C):
    p = C.p

    @p.setter
    def p(self, val):
        print 'Implement extra functionality here for E'
        C.p.fset(self, val)

Die Verwendung von super () ist jedoch ziemlich umständlich, da die Eigenschaft neu definiert werden muss und Sie den leicht kontraintuitiven Mechanismus super (cls, cls) verwenden müssen, um eine ungebundene Kopie von p zu erhalten.

NeilenMarais
quelle
20

Die Verwendung von Eigenschaften ist für mich intuitiver und passt besser in den meisten Code.

Vergleichen

o.x = 5
ox = o.x

vs.

o.setX(5)
ox = o.getX()

ist für mich ganz offensichtlich was leichter zu lesen ist. Auch Eigenschaften ermöglichen private Variablen viel einfacher.

Hobblin
quelle
12

In den meisten Fällen würde ich lieber keine verwenden. Das Problem mit Eigenschaften ist, dass sie die Klasse weniger transparent machen. Dies ist insbesondere dann ein Problem, wenn Sie eine Ausnahme von einem Setter auslösen. Wenn Sie beispielsweise eine Account.email-Eigenschaft haben:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

dann erwartet der Benutzer der Klasse nicht, dass das Zuweisen eines Werts zur Eigenschaft eine Ausnahme verursachen könnte:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

Infolgedessen kann die Ausnahme unbehandelt bleiben und sich entweder zu hoch in der Aufrufkette ausbreiten, um ordnungsgemäß behandelt zu werden, oder dazu führen, dass dem Programmbenutzer ein sehr wenig hilfreiches Traceback angezeigt wird (was in der Welt von Python und Java leider zu häufig ist ).

Ich würde auch vermeiden, Getter und Setter zu verwenden:

  • weil es sehr zeitaufwändig ist, sie für alle Eigenschaften im Voraus zu definieren,
  • macht die Codemenge unnötig länger, was das Verständnis und die Pflege des Codes erschwert.
  • Wenn Sie sie nur nach Bedarf für Eigenschaften definieren würden, würde sich die Schnittstelle der Klasse ändern und alle Benutzer der Klasse verletzen

Anstelle von Eigenschaften und Gettern / Setzern bevorzuge ich die komplexe Logik an genau definierten Stellen, z. B. in einer Validierungsmethode:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

oder eine ähnliche Account.save-Methode.

Beachten Sie, dass ich nicht versuche zu sagen, dass es keine Fälle gibt, in denen Eigenschaften nützlich sind, sondern nur, dass Sie möglicherweise besser dran sind, wenn Sie Ihre Klassen so einfach und transparent gestalten können, dass Sie sie nicht benötigen.


quelle
3
@ user2239734 Ich denke, Sie verstehen das Konzept der Eigenschaften falsch. Obwohl Sie den Wert beim Festlegen der Eigenschaft überprüfen können, ist dies nicht erforderlich. Sie können sowohl Eigenschaften als auch eine validate()Methode in einer Klasse haben. Eine Eigenschaft wird einfach verwendet, wenn Sie eine komplexe Logik hinter einer einfachen obj.x = yZuweisung haben und es an der Logik liegt.
Zaur Nasibov
12

Ich habe das Gefühl, dass es bei Eigenschaften darum geht, dass Sie den Aufwand für das Schreiben von Gettern und Setzern nur dann erhalten, wenn Sie sie tatsächlich benötigen.

In der Java-Programmierkultur wird dringend empfohlen, niemals Zugriff auf Eigenschaften zu gewähren und stattdessen Getter und Setter zu verwenden, und nur diejenigen, die tatsächlich benötigt werden. Es ist etwas ausführlich, diese offensichtlichen Codeteile immer zu schreiben und zu beachten, dass sie in 70% der Fälle nie durch eine nicht triviale Logik ersetzt werden.

In Python kümmern sich die Leute tatsächlich um diese Art von Overhead, sodass Sie die folgende Praxis anwenden können:

  • Verwenden Sie zuerst keine Getter und Setter, wenn diese nicht benötigt werden
  • Verwenden Sie @propertydiese Option , um sie zu implementieren, ohne die Syntax des restlichen Codes zu ändern.
Fulmicoton
quelle
1
"und beachten Sie, dass sie in 70% der Fälle nie durch eine nicht triviale Logik ersetzt werden." - das ist eine ziemlich spezifische Zahl, kommt sie von irgendwoher, oder beabsichtigen Sie sie als handgewellte "die überwiegende Mehrheit" (ich bin nicht scherzhaft, wenn es eine Studie gibt, die diese Zahl quantifiziert, wäre ich es wirklich daran interessiert, es zu lesen)
Adam Parkin
1
Oh nein, tut mir leid. Es hört sich so an, als hätte ich eine Studie, um diese Nummer zu sichern, aber ich meinte es nur als "die meiste Zeit".
Fulmicoton
7
Es ist nicht so, dass sich die Leute um den Overhead kümmern, sondern dass Sie in Python vom direkten Zugriff auf Zugriffsmethoden wechseln können, ohne den Client-Code zu ändern. Sie haben also nichts zu verlieren, wenn Sie zuerst Eigenschaften direkt verfügbar machen.
Neil G
10

Ich bin überrascht, dass niemand erwähnt hat, dass Eigenschaften gebundene Methoden einer Deskriptorklasse sind. Adam Donohue und NeilenMarais kommen in ihren Beiträgen genau auf diese Idee - dass Getter und Setter Funktionen sind und verwendet werden können, um:

  • bestätigen
  • Daten ändern
  • Ententyp (Zwangstyp zu einem anderen Typ)

Dies stellt eine intelligente Art und Weise zu verstecken Details der Implementierung und Code cruft wie reguläre Ausdrücke, Typ Abgüsse, try .. außer Blöcke, Behauptungen oder berechneten Werte.

Im Allgemeinen kann das Ausführen von CRUD für ein Objekt häufig recht banal sein. Betrachten Sie jedoch das Beispiel von Daten, die in einer relationalen Datenbank gespeichert werden. ORMs können Implementierungsdetails bestimmter SQL-Umgangssprachen in den Methoden verbergen, die an fget, fset, fdel gebunden sind, die in einer Eigenschaftsklasse definiert sind, die das Schreckliche verwaltet, wenn .. elif .. sonst Leitern, die im OO-Code so hässlich sind - das Einfache und elegant self.variable = somethingund vermeiden Sie die Details für den Entwickler mit dem ORM.

Wenn man Eigenschaften nur als einen trostlosen Überrest einer Bondage- und Disziplin-Sprache (dh Java) betrachtet, fehlt ihnen der Punkt der Deskriptoren.

Fiaker
quelle
6

In komplexen Projekten bevorzuge ich schreibgeschützte Eigenschaften (oder Getter) mit expliziter Setter-Funktion:

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

def set_my_attr(self, value):
    ...

In langlebigen Projekten dauert das Debuggen und Refactoring länger als das Schreiben des Codes selbst. Die Verwendung hat mehrere Nachteile, @property.setterdie das Debuggen noch schwieriger machen:

1) Python ermöglicht das Erstellen neuer Attribute für ein vorhandenes Objekt. Dies macht es sehr schwierig, einen folgenden Druckfehler zu verfolgen:

my_object.my_atttr = 4.

Wenn es sich bei Ihrem Objekt um einen komplizierten Algorithmus handelt, werden Sie einige Zeit damit verbringen, herauszufinden, warum es nicht konvergiert (beachten Sie ein zusätzliches 't' in der obigen Zeile).

2) Setter kann sich manchmal zu einer komplizierten und langsamen Methode entwickeln (z. B. das Schlagen einer Datenbank). Für einen anderen Entwickler wäre es ziemlich schwierig herauszufinden, warum die folgende Funktion sehr langsam ist. Möglicherweise verbringt er viel Zeit mit der Profilerstellung do_something(), während dies my_object.my_attr = 4.tatsächlich die Ursache für die Verlangsamung ist:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()
otognan
quelle
5

Sowohl @propertyals auch traditionelle Getter und Setter haben ihre Vorteile. Dies hängt von Ihrem Anwendungsfall ab.

Vorteile von @property

  • Sie müssen die Schnittstelle nicht ändern, während Sie die Implementierung des Datenzugriffs ändern. Wenn Ihr Projekt klein ist, möchten Sie wahrscheinlich den direkten Attributzugriff verwenden, um auf ein Klassenmitglied zuzugreifen. Angenommen, Sie haben ein Objekt foovom Typ Foo, das ein Mitglied hat num. Dann können Sie dieses Mitglied einfach mit bekommen num = foo.num. Wenn Ihr Projekt wächst, haben Sie möglicherweise das Gefühl, dass der einfache Attributzugriff überprüft oder behoben werden muss. Dann können Sie das mit einem @property innerhalb der Klasse tun . Die Datenzugriffsschnittstelle bleibt unverändert, sodass der Clientcode nicht geändert werden muss.

    Zitiert aus PEP-8 :

    Für einfache öffentliche Datenattribute ist es am besten, nur den Attributnamen ohne komplizierte Accessor / Mutator-Methoden verfügbar zu machen. Beachten Sie, dass Python einen einfachen Weg zur zukünftigen Verbesserung bietet, falls Sie feststellen, dass ein einfaches Datenattribut das Funktionsverhalten verbessern muss. Verwenden Sie in diesem Fall Eigenschaften, um die funktionale Implementierung hinter der einfachen Datenattributzugriffssyntax zu verbergen.

  • Die Verwendung @propertyfür den Datenzugriff in Python wird als Pythonic angesehen :

    • Es kann Ihre Selbstidentifikation als Python-Programmierer (nicht Java-Programmierer) stärken.

    • Es kann Ihrem Vorstellungsgespräch helfen, wenn Ihr Interviewer der Meinung ist, dass Getter und Setter im Java-Stil Anti-Patterns sind .

Vorteile traditioneller Getter und Setter

  • Herkömmliche Getter und Setter ermöglichen einen komplizierteren Datenzugriff als den einfachen Attributzugriff. Wenn Sie beispielsweise ein Klassenmitglied festlegen, benötigen Sie manchmal ein Flag, das angibt, wo Sie diesen Vorgang erzwingen möchten, auch wenn etwas nicht perfekt aussieht. Während es nicht offensichtlich ist, wie ein direkter Mitgliederzugriff wie erweitert foo.num = numwerden kann, können Sie Ihren herkömmlichen Setter einfach um einen zusätzlichen forceParameter erweitern:

    def Foo:
        def set_num(self, num, force=False):
            ...
  • Herkömmliche Getter und Setter machen deutlich, dass der Zugriff eines Klassenmitglieds über eine Methode erfolgt. Das heisst:

    • Was Sie als Ergebnis erhalten, stimmt möglicherweise nicht mit dem überein, was genau in dieser Klasse gespeichert ist.

    • Selbst wenn der Zugriff wie ein einfacher Attributzugriff aussieht, kann die Leistung stark davon abweichen.

    Wenn Ihre Klassenbenutzer nicht erwarten, dass @propertysich hinter jeder Attributzugriffsanweisung ein Versteck verbirgt, kann die explizite Darstellung solcher Dinge dazu beitragen, die Überraschungen Ihrer Klassenbenutzer zu minimieren.

  • Wie von @NeilenMarais und in diesem Beitrag erwähnt , ist das Erweitern traditioneller Getter und Setter in Unterklassen einfacher als das Erweitern von Eigenschaften.

  • Traditionelle Getter und Setter sind seit langem in verschiedenen Sprachen weit verbreitet. Wenn Sie Leute mit unterschiedlichem Hintergrund in Ihrem Team haben, sehen sie vertrauter aus als @property. Wenn Ihr Projekt wächst und Sie möglicherweise von Python in eine andere Sprache migrieren müssen, die es nicht gibt @property, würde die Verwendung herkömmlicher Getter und Setter die Migration reibungsloser gestalten.

Vorsichtsmaßnahmen

  • Weder @propertyherkömmliche Getter und Setter machen das Klassenmitglied privat, selbst wenn Sie vor seinem Namen einen doppelten Unterstrich verwenden:

    class Foo:
        def __init__(self):
            self.__num = 0
    
        @property
        def num(self):
            return self.__num
    
        @num.setter
        def num(self, num):
            self.__num = num
    
        def get_num(self):
            return self.__num
    
        def set_num(self, num):
            self.__num = num
    
    foo = Foo()
    print(foo.num)          # output: 0
    print(foo.get_num())    # output: 0
    print(foo._Foo__num)    # output: 0
Cyker
quelle
2

Hier ist ein Auszug aus "Effektives Python: 90 spezifische Möglichkeiten, besseres Python zu schreiben" (Erstaunliches Buch. Ich kann es nur empfehlen).

Dinge, an die man sich erinnern sollte

✦ Definieren Sie neue Klassenschnittstellen mit einfachen öffentlichen Attributen und vermeiden Sie die Definition von Setter- und Getter-Methoden.

✦ Verwenden Sie @property, um bei Bedarf ein spezielles Verhalten beim Zugriff auf Attribute für Ihre Objekte zu definieren.

✦ Befolgen Sie die Regel der geringsten Überraschung und vermeiden Sie seltsame Nebenwirkungen bei Ihren @ property-Methoden.

✦ Stellen Sie sicher, dass die @ property-Methoden schnell sind. Verwenden Sie für langsame oder komplexe Arbeiten - insbesondere mit E / A oder Nebenwirkungen - stattdessen normale Methoden.

Eine fortgeschrittene, aber häufig verwendete Verwendung von @property ist die Umwandlung eines ehemals einfachen numerischen Attributs in eine On-the-Fly-Berechnung. Dies ist äußerst hilfreich, da Sie damit die gesamte vorhandene Verwendung einer Klasse migrieren können, um neue Verhaltensweisen zu erhalten, ohne dass eine der Aufrufseiten neu geschrieben werden muss (was besonders wichtig ist, wenn Sie Code aufrufen, den Sie nicht steuern). @property bietet auch eine wichtige Lücke zur Verbesserung der Schnittstellen im Laufe der Zeit.

Ich mag @property besonders, weil Sie damit im Laufe der Zeit schrittweise Fortschritte in Richtung eines besseren Datenmodells erzielen können.
@property ist ein Tool, mit dem Sie Probleme beheben können, auf die Sie im realen Code stoßen. Überbeanspruchen Sie es nicht. Wenn Sie feststellen, dass Sie die @ property-Methoden wiederholt erweitern, ist es wahrscheinlich an der Zeit, Ihre Klasse umzugestalten, anstatt das schlechte Design Ihres Codes weiter zu verbessern.

✦ Verwenden Sie @property, um vorhandenen Instanzattributen neue Funktionen zu geben.

✦ Machen Sie mithilfe von @property schrittweise Fortschritte in Richtung besserer Datenmodelle.

✦ Überlegen Sie, eine Klasse und alle Aufrufseiten umzugestalten, wenn Sie @property zu häufig verwenden.

Vlad Bezden
quelle