Dies wird eine langwierige Antwort sein, die möglicherweise nur als Komplementär dient ... aber Ihre Frage hat mich zu einer Fahrt durch das Kaninchenloch geführt, sodass ich auch meine Erkenntnisse (und Schmerzen) mitteilen möchte.
Möglicherweise ist diese Antwort für Ihr eigentliches Problem nicht hilfreich. In der Tat ist meine Schlussfolgerung, dass - ich würde das überhaupt nicht tun. Der Hintergrund dieser Schlussfolgerung könnte Sie jedoch ein wenig unterhalten, da Sie nach weiteren Details suchen.
Ein Missverständnis ansprechen
Die erste Antwort ist zwar in den meisten Fällen richtig, aber nicht immer der Fall. Betrachten Sie zum Beispiel diese Klasse:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
ist ein property
unwiderrufliches Instanzattribut:
>>> Foo.inst_prop
Traceback (most recent call last):
File "<pyshell#60>", line 1, in <module>
Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'
Es hängt alles davon , wo Ihr property
in der ersten Stelle definiert. Wenn Ihr @property
innerhalb der Klasse "scope" (oder wirklich der namespace
) definiert ist, wird es zu einem Klassenattribut. In meinem Beispiel sind der Klasse selbst keine bekannt, inst_prop
bis sie instanziiert wird. Natürlich ist es als Immobilie hier überhaupt nicht sehr nützlich.
Aber lassen Sie uns zuerst Ihren Kommentar zur Auflösung der Vererbung ansprechen ...
Wie genau spielt die Vererbung in diesem Problem eine Rolle? Dieser folgende Artikel befasst sich ein wenig mit dem Thema, und die Reihenfolge der Methodenauflösung ist etwas verwandt, obwohl hauptsächlich die Breite der Vererbung anstelle der Tiefe behandelt wird.
Kombiniert mit unserem Ergebnis, angesichts der folgenden Einstellungen:
@property
def some_prop(self):
return "Family property"
class Grandparent:
culture = some_prop
world_view = some_prop
class Parent(Grandparent):
world_view = "Parent's new world_view"
class Child(Parent):
def __init__(self):
try:
self.world_view = "Child's new world_view"
self.culture = "Child's new culture"
except AttributeError as exc:
print(exc)
self.__dict__['culture'] = "Child's desired new culture"
Stellen Sie sich vor, was passiert, wenn diese Zeilen ausgeführt werden:
print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Das Ergebnis ist also:
Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>
Beachte wie:
self.world_view
konnte angewendet werden, während self.culture
fehlgeschlagen
culture
existiert nicht in Child.__dict__
(der mappingproxy
Klasse, nicht zu verwechseln mit der Instanz __dict__
)
- Obwohl in
culture
existiert c.__dict__
, wird nicht darauf verwiesen.
Möglicherweise können Sie erraten, warum - world_view
wurde von der Parent
Klasse als Nicht-Eigenschaft Child
überschrieben , sodass Sie es auch überschreiben konnten. In der Zwischenzeit existiert es, da culture
es vererbt wird, nur innerhalb mappingproxy
vonGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
In der Tat, wenn Sie versuchen zu entfernen Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Sie werden feststellen, dass es nicht einmal existiert Parent
. Weil sich das Objekt direkt auf verweist Grandparent.culture
.
Was ist also mit der Resolution Order?
Wir sind also daran interessiert, die tatsächliche Auflösungsreihenfolge zu beachten. Versuchen wir Parent.world_view
stattdessen , Folgendes zu entfernen :
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Frage mich, was das Ergebnis ist?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Es kehrte zu Großeltern zurück world_view
property
, obwohl wir es erfolgreich geschafft hatten, das self.world_view
vorher zuzuweisen ! Aber was ist, wenn wir uns world_view
auf Klassenebene gewaltsam ändern , wie die andere Antwort? Was ist, wenn wir es löschen? Was ist, wenn wir das aktuelle Klassenattribut einer Eigenschaft zuweisen?
Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Das Ergebnis ist:
# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view
# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view
# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>
Dies ist interessant, da das c.world_view
Instanzattribut wiederhergestellt wird, während Child.world_view
es das von uns zugewiesene ist. Nach dem Entfernen des Instanzattributs wird auf das Klassenattribut zurückgegriffen. Und nachdem Child.world_view
wir das der Eigenschaft neu zugewiesen haben , verlieren wir sofort den Zugriff auf das Instanzattribut.
Daher können wir die folgende Auflösungsreihenfolge vermuten :
- Wenn ein Klassenattribut vorhanden ist und es ein ist
property
, rufen Sie seinen Wert über getter
oder ab fget
(dazu später mehr). Aktuelle Klasse zuerst bis Basisklasse zuletzt.
- Wenn andernfalls ein Instanzattribut vorhanden ist, rufen Sie den Instanzattributwert ab.
- Andernfalls rufen Sie das Nichtklassenattribut ab
property
. Aktuelle Klasse zuerst bis Basisklasse zuletzt.
In diesem Fall entfernen wir die Wurzel property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Welches gibt:
c.culture is: Child's desired new culture
Traceback (most recent call last):
File "<pyshell#74>", line 1, in <module>
print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'
TA Dah! Child
hat jetzt ihre eigene culture
basierend auf dem kraftvollen Einfügen in c.__dict__
. Child.culture
existiert natürlich nicht, da es nie in Parent
oder Child
Klassenattribut definiert wurde und Grandparent
's entfernt wurde.
Ist das die Hauptursache für mein Problem?
Eigentlich nein . Der Fehler, den Sie beim Zuweisen immer noch beobachten self.culture
, ist völlig anders . Aber die Vererbungsreihenfolge bildet den Hintergrund für die Antwort - die das property
Selbst ist.
Haben Sie neben der zuvor erwähnten getter
Methode property
auch ein paar nette Tricks im Ärmel. Am relevantesten ist in diesem Fall die Methode setter
oder fset
, die durch die self.culture = ...
Leitung ausgelöst wird . Da Sie property
keine setter
oder keine fget
Funktion implementiert haben , weiß Python nicht, was zu tun ist, und wirft AttributeError
stattdessen eine (dh can't set attribute
).
Wenn Sie jedoch eine setter
Methode implementiert haben :
@property
def some_prop(self):
return "Family property"
@some_prop.setter
def some_prop(self, val):
print(f"property setter is called!")
# do something else...
Wenn Sie die Child
Klasse instanziieren , erhalten Sie:
Instantiating Child class...
property setter is called!
Anstatt eine zu erhalten AttributeError
, rufen Sie jetzt tatsächlich die some_prop.setter
Methode auf. Dadurch haben Sie mehr Kontrolle über Ihr Objekt. Aufgrund unserer früheren Erkenntnisse wissen wir, dass ein Klassenattribut überschrieben werden muss, bevor es die Eigenschaft erreicht. Dies könnte innerhalb der Basisklasse als Trigger implementiert werden. Hier ist ein neues Beispiel:
class Grandparent:
@property
def culture(self):
return "Family property"
# add a setter method
@culture.setter
def culture(self, val):
print('Fine, have your own culture')
# overwrite the child class attribute
type(self).culture = None
self.culture = val
class Parent(Grandparent):
pass
class Child(Parent):
def __init__(self):
self.culture = "I'm a millennial!"
c = Child()
print(c.culture)
Was in ... resultiert:
Fine, have your own culture
I'm a millennial!
TA DAH! Sie können jetzt Ihr eigenes Instanzattribut über eine geerbte Eigenschaft überschreiben!
Also, Problem gelöst?
... Nicht wirklich. Das Problem bei diesem Ansatz ist, dass Sie jetzt keine richtige setter
Methode haben können. Es gibt Fälle, in denen Sie Werte für Ihre festlegen möchten property
. Aber jetzt , wenn stellen Sie self.culture = ...
sie immer überschreiben , was Funktion , die Sie in der definiert getter
(was in diesem Fall wirklich nur der ist @property
umschlungenen Bereich. Sie können in differenzierteren Maßnahmen hinzufügen, aber eine oder andere Weise , es wird immer mehr als nur beinhalten self.culture = ...
. z.B:
class Grandparent:
# ...
@culture.setter
def culture(self, val):
if isinstance(val, tuple):
if val[1]:
print('Fine, have your own culture')
type(self).culture = None
self.culture = val[0]
else:
raise AttributeError("Oh no you don't")
# ...
class Child(Parent):
def __init__(self):
try:
# Usual setter
self.culture = "I'm a Gen X!"
except AttributeError:
# Trigger the overwrite condition
self.culture = "I'm a Boomer!", True
Es ist auf Klassenebene komplizierter als die andere Antwort size = None
.
Sie können auch einen eigenen Deskriptor schreiben , um die Methoden __get__
und __set__
oder zusätzliche Methoden zu verarbeiten. Aber am Ende des Tages, wenn self.culture
verwiesen wird, __get__
wird immer zuerst ausgelöst, und wenn self.culture = ...
verwiesen wird, __set__
wird immer zuerst ausgelöst. Soweit ich es versucht habe, führt kein Weg daran vorbei.
Der Kern des Problems, IMO
Das Problem, das ich hier sehe, ist - Sie können Ihren Kuchen nicht haben und ihn auch essen. property
ist wie ein Deskriptor mit bequemem Zugriff von Methoden wie getattr
oder gemeint setattr
. Wenn Sie auch möchten, dass diese Methoden einen anderen Zweck erfüllen, fragen Sie nur nach Problemen. Ich würde vielleicht den Ansatz überdenken:
- Benötige ich wirklich eine
property
dafür?
- Könnte mir eine Methode anders dienen?
- Wenn ich eine brauche
property
, gibt es einen Grund, warum ich sie überschreiben müsste?
- Gehört die Unterklasse wirklich zur selben Familie, wenn diese
property
nicht zutreffen?
- Wenn ich ein oder alle
property
s überschreiben muss , würde mir eine separate Methode besser dienen als nur eine Neuzuweisung, da eine Neuzuweisung die property
s versehentlich ungültig machen kann ?
Für Punkt 5 wäre mein Ansatz eine overwrite_prop()
Methode in der Basisklasse, die das aktuelle Klassenattribut überschreibt, so dass das property
nicht mehr ausgelöst wird:
class Grandparent:
# ...
def overwrite_props(self):
# reassign class attributes
type(self).size = None
type(self).len = None
# other properties, if necessary
# ...
# Usage
class Child(Parent):
def __init__(self):
self.overwrite_props()
self.size = 5
self.len = 10
Wie Sie sehen können, ist es, obwohl es noch ein bisschen erfunden ist, zumindest expliziter als ein kryptisches size = None
. Letztendlich würde ich die Eigenschaft jedoch überhaupt nicht überschreiben und mein Design von Grund auf überdenken.
Wenn Sie es bis hierher geschafft haben - danke, dass Sie diese Reise mit mir gegangen sind. Es war eine lustige kleine Übung.