Questa sarà una risposta lunga e tortuosa che potrebbe solo essere gratuita ... ma la tua domanda mi ha portato a fare un giro nella tana del coniglio, quindi vorrei condividere anche i miei risultati (e il dolore).
Alla fine potresti trovare questa risposta non utile al tuo vero problema. In effetti, la mia conclusione è che non lo farei affatto. Detto questo, lo sfondo di questa conclusione potrebbe intrattenerti un po ', dal momento che stai cercando maggiori dettagli.
Affrontare alcuni malintesi
La prima risposta, sebbene corretta nella maggior parte dei casi, non è sempre il caso. Ad esempio, considera questa classe:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop, pur essendo a property, è irrevocabilmente un attributo di istanza:
>>> 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!'
Tutto dipende da dove il tuo propertyè definito in primo luogo. Se il tuo @propertyè definito nella classe "scope" (o davvero, il namespace), diventa un attributo di classe. Nel mio esempio, la classe stessa non ne è a conoscenza inst_propfino a quando non viene istanziata. Naturalmente, non è molto utile come proprietà qui.
Ma prima, affrontiamo il tuo commento sulla risoluzione dell'ereditarietà ...
Quindi, in che modo l'ereditarietà tiene conto di questo problema? Questo articolo che segue approfondisce un po 'l'argomento e l' Ordine di risoluzione del metodo è in qualche modo correlato, anche se discute principalmente sull'ampiezza dell'eredità anziché sulla profondità.
Insieme alla nostra scoperta, date queste impostazioni di seguito:
@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"
Immagina cosa succede quando vengono eseguite queste linee:
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}')
Il risultato è quindi:
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>
Si noti come:
self.world_viewè stato possibile applicare, anche se self.culturenon riuscito
culturenon esiste in Child.__dict__(il mappingproxydella classe, da non confondere con l'istanza __dict__)
- Anche se
cultureesiste in c.__dict__, non è referenziato.
Potresti essere in grado di indovinare perché - è world_viewstato sovrascritto dalla Parentclasse come non-proprietà, quindi è Childstato anche in grado di sovrascriverlo. Nel frattempo, poiché cultureè ereditato, esiste solo all'interno mappingproxydiGrandparent :
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
Infatti se provi a rimuovere Parent.culture:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Noterai che non esiste nemmeno per Parent. Perché l'oggetto fa direttamente riferimento a Grandparent.culture.
Quindi, per quanto riguarda l'ordine di risoluzione?
Quindi siamo interessati a osservare l'attuale Ordine di risoluzione, proviamo Parent.world_viewinvece a rimuovere :
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
Mi chiedo quale sia il risultato?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
È tornato ai nonni world_view property, anche se siamo riusciti a assegnare il self.world_viewprima! E se cambiassimo con forza world_viewa livello di classe, come l'altra risposta? E se lo eliminassimo? Cosa succede se assegniamo l'attributo di classe corrente a una proprietà?
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}')
Il risultato è:
# 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>
Questo è interessante perché c.world_viewviene ripristinato il suo attributo di istanza, mentre Child.world_viewè quello che abbiamo assegnato. Dopo aver rimosso l'attributo dell'istanza, ritorna all'attributo class. E dopo aver riassegnato la Child.world_viewproprietà, perdiamo immediatamente l'accesso all'attributo dell'istanza.
Pertanto, possiamo ipotizzare il seguente ordine di risoluzione :
- Se esiste un attributo di classe ed è un
property, recuperane il valore tramite gettero fget(ne parleremo più avanti). Prima la classe corrente per ultima.
- Altrimenti, se esiste un attributo di istanza, recuperare il valore dell'attributo di istanza.
- Altrimenti, recupera l'
propertyattributo non di classe. Prima la classe corrente per ultima.
In tal caso, rimuoviamo il root property:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Che dà:
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! Childora ha il proprio culturebasato sull'inserimento forzato in c.__dict__. Child.culturenon esiste, ovviamente, poiché non è mai stato definito in Parento Childattributo di classe ed Grandparentè stato rimosso.
È questa la causa principale del mio problema?
In realtà no . L'errore che stai riscontrando, che stiamo ancora osservando durante l'assegnazione self.culture, è totalmente diverso . Ma l'ordine di eredità fa da sfondo alla risposta, che è la propertystessa.
Oltre al gettermetodo precedentemente citato , propertyhai anche qualche asso nella manica. Il più rilevante in questo caso è il metodo, settero fset, che viene attivato dalla self.culture = ...riga. Dato che propertynon hai implementato nessuna settero nessuna fgetfunzione, python non sa cosa fare e lancia AttributeErrorinvece (cioè can't set attribute).
Se tuttavia hai implementato un settermetodo:
@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...
Quando crei un'istanza della Childclasse otterrai:
Instantiating Child class...
property setter is called!
Invece di ricevere un AttributeError, ora stai effettivamente chiamando il some_prop.settermetodo. Il che ti dà un maggiore controllo sul tuo oggetto ... con i nostri risultati precedenti, sappiamo che è necessario sovrascrivere un attributo di classe prima che raggiunga la proprietà. Questo potrebbe essere implementato all'interno della classe base come trigger. Ecco un nuovo esempio:
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)
Che si traduce in:
Fine, have your own culture
I'm a millennial!
TA-DAH! Ora puoi sovrascrivere il tuo attributo di istanza su una proprietà ereditata!
Quindi, problema risolto?
... Non proprio. Il problema con questo approccio è che ora non puoi avere un settermetodo adeguato . Ci sono casi in cui vuoi impostare valori sul tuo property. Ma ora ogni volta che si imposta self.culture = ...sarà sempre sovrascriverà qualsiasi funzione definita nella getter(che in questo caso, è in realtà solo la @propertyporzione avvolta. È possibile aggiungere misure più sfumata, ma un modo o nell'altro lo sarai sempre coinvolgere più di un semplice self.culture = .... per esempio:
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
È waaaaay più complicato dell'altra risposta, size = Nonea livello di classe.
Si potrebbe anche considerare scrivere il proprio descrittore , invece di gestire l' __get__e __set__, o metodi aggiuntivi. Ma alla fine della giornata, quando si self.culturefa riferimento, __get__verrà sempre innescato per primo, e quando si self.culture = ...fa riferimento, __set__sarà sempre innescato per primo. Non c'è modo di aggirarlo per quanto ho provato.
Il nocciolo della questione, IMO
Il problema che vedo qui è: non puoi avere la tua torta e mangiarla anche tu. propertysi intende come un descrittore con comodo accesso da metodi come getattro setattr. Se vuoi che anche questi metodi raggiungano uno scopo diverso, stai solo chiedendo problemi. Vorrei forse ripensare l'approccio:
- Ho davvero bisogno di un
propertyper questo?
- Un metodo potrebbe servirmi in modo diverso?
- Se ho bisogno di un
property, c'è qualche motivo per cui dovrei sovrascriverlo?
- La sottoclasse appartiene davvero alla stessa famiglia se questi
propertynon si applicano?
- Se avessi bisogno di sovrascrivere qualsiasi / tutti gli
propertys, un metodo separato mi servirebbe meglio della semplice riassegnazione, dal momento che la riassegnazione può annullare accidentalmente la propertys?
Per il punto 5, il mio approccio dovrebbe avere un overwrite_prop()metodo nella classe base che sovrascriva l'attributo della classe corrente in modo che il propertytestamento non venga più attivato:
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
Come puoi vedere, sebbene sia ancora un po 'inventato, è almeno più esplicito di un criptico size = None. Detto questo, in definitiva, non avrei sovrascritto la proprietà e avrei riconsiderato il mio progetto dalla radice.
Se sei arrivato così lontano, grazie per aver fatto questo viaggio con me. È stato un piccolo esercizio divertente.