Esta será una respuesta larga y sin aliento que solo podría ser complementaria ... pero su pregunta me llevó a dar una vuelta por la madriguera del conejo, así que también me gustaría compartir mis hallazgos (y dolor).
En última instancia, puede encontrar esta respuesta no útil para su problema real. De hecho, mi conclusión es que no haría esto en absoluto. Dicho esto, los antecedentes de esta conclusión pueden entretenerlo un poco, ya que está buscando más detalles.
Abordar algunos conceptos erróneos
La primera respuesta, aunque correcta en la mayoría de los casos, no siempre es el caso. Por ejemplo, considere esta clase:
class Foo:
def __init__(self):
self.name = 'Foo!'
@property
def inst_prop():
return f'Retrieving {self.name}'
self.inst_prop = inst_prop
inst_prop
, aunque es un property
, es irrevocablemente un atributo de instancia:
>>> 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!'
Todo depende donde sus property
se define en el primer lugar. Si su @property
se define dentro de la clase "ámbito" (o realmente, el namespace
), se convierte en un atributo de clase. En mi ejemplo, la clase en sí misma no conoce ninguna inst_prop
hasta que se instancia. Por supuesto, no es muy útil como propiedad aquí.
Pero primero, abordemos su comentario sobre la resolución de herencia ...
Entonces, ¿cómo influye exactamente la herencia en este problema? El siguiente artículo se sumerge un poco en el tema, y el Orden de resolución de métodos está algo relacionado, aunque analiza principalmente la amplitud de la herencia en lugar de la profundidad.
En combinación con nuestro hallazgo, dada la siguiente configuración:
@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"
Imagine lo que sucede cuando se ejecutan estas líneas:
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}')
El resultado es así:
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>
Date cuenta cómo:
self.world_view
se pudo aplicar, aunque self.culture
falló
culture
no existe en Child.__dict__
(el mappingproxy
de la clase, no debe confundirse con la instancia __dict__
)
- Aunque
culture
existe en c.__dict__
, no se hace referencia a él.
Es posible que pueda adivinar por qué: la clase la world_view
sobrescribió Parent
como no propiedad, por Child
lo que también pude sobrescribirla. Mientras tanto, como culture
se hereda, solo existe dentro mappingproxy
deGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
De hecho, si intentas eliminar Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Notarás que ni siquiera existe para Parent
. Porque el objeto se está refiriendo directamente a Grandparent.culture
.
Entonces, ¿qué pasa con la orden de resolución?
Por lo tanto, estamos interesados en observar el orden de resolución real, intentemos eliminarlo Parent.world_view
:
del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
¿Se pregunta cuál es el resultado?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
¡Volvió a los abuelos world_view
property
, a pesar de que habíamos logrado asignar el self.world_view
antes! Pero, ¿qué pasa si cambiamos con fuerza world_view
a nivel de clase, como la otra respuesta? ¿Qué pasa si lo eliminamos? ¿Qué sucede si asignamos el atributo de clase actual como una propiedad?
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}')
El resultado es:
# 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>
Esto es interesante porque c.world_view
se restaura a su atributo de instancia, mientras que Child.world_view
es el que le asignamos. Después de eliminar el atributo de instancia, vuelve al atributo de clase. Y después de reasignar Child.world_view
a la propiedad, perdemos instantáneamente el acceso al atributo de instancia.
Por lo tanto, podemos suponer el siguiente orden de resolución :
- Si existe un atributo de clase y es a
property
, recupere su valor mediante getter
o fget
(más sobre esto más adelante). La clase actual primero a la clase base última.
- De lo contrario, si existe un atributo de instancia, recupere el valor del atributo de instancia.
- De lo contrario, recupere el
property
atributo que no es de clase. La clase actual primero a la clase base última.
En ese caso, eliminemos la raíz property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Lo que da:
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
ahora tiene el suyo culture
basado en la inserción forzada en c.__dict__
. Child.culture
no existe, por supuesto, ya que nunca se definió en Parent
o Child
atributo de clase, y Grandparent
se eliminó.
¿Es esta la causa raíz de mi problema?
En realidad no . El error que está recibiendo, que todavía estamos observando al asignar self.culture
, es totalmente diferente . Pero el orden de herencia establece el telón de fondo para la respuesta, que es la property
propia.
Además del getter
método mencionado anteriormente , property
también tiene algunos buenos trucos bajo la manga. Lo más relevante en este caso es el método setter
o fset
, que se activa por self.culture = ...
línea. Como property
no implementó ninguna función setter
o fget
función, python no sabe qué hacer, y en su AttributeError
lugar arroja un (es decir can't set attribute
).
Sin embargo, si implementó un setter
método:
@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...
Al crear una instancia de la Child
clase, obtendrá:
Instantiating Child class...
property setter is called!
En lugar de recibir un AttributeError
, ahora estás llamando al some_prop.setter
método. Lo que le da más control sobre su objeto ... con nuestros hallazgos anteriores, sabemos que necesitamos tener un atributo de clase sobrescrito antes de que llegue a la propiedad. Esto podría implementarse dentro de la clase base como desencadenante. Aquí hay un nuevo ejemplo:
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)
Lo que resulta en:
Fine, have your own culture
I'm a millennial!
TA-DAH! ¡Ahora puede sobrescribir su propio atributo de instancia sobre una propiedad heredada!
Entonces, ¿problema resuelto?
... Realmente no. El problema con este enfoque es que ahora no puede tener un setter
método adecuado . Hay casos en los que desea establecer valores en su property
. Pero ahora cada vez que se establece self.culture = ...
que será siempre sobrescribir cualquier función que ha definido en el getter
(que en este caso, realmente es sólo la @property
parte envuelta. Usted puede añadir en medidas más matizadas, pero de una manera u otra siempre va a implicar más que simplemente self.culture = ...
. p.ej:
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 muuuucho más complicado que la otra respuesta, size = None
a nivel de clase.
También podría considerar escribir su propio descriptor en vez de manejar la __get__
e __set__
, o métodos adicionales. Pero al final del día, cuando self.culture
se hace referencia, __get__
siempre se activará primero, y cuando self.culture = ...
se haga referencia, __set__
siempre se activará primero. No hay forma de evitarlo por lo que he intentado.
El quid de la cuestión, la OMI
El problema que veo aquí es que no puedes tener tu pastel y comértelo también. property
se entiende como un descriptor con acceso conveniente desde métodos como getattr
o setattr
. Si también desea que estos métodos logren un propósito diferente, solo está buscando problemas. Quizás debería repensar el enfoque:
- ¿Realmente necesito un
property
para esto?
- ¿Podría un método servirme de manera diferente?
- Si necesito un
property
, ¿hay alguna razón por la que deba sobrescribirlo?
- ¿La subclase realmente pertenece a la misma familia si
property
no se aplica?
- Si necesito sobrescribir alguno / todos los
property
s, ¿un método separado me serviría mejor que simplemente reasignar, ya que la reasignación puede anular accidentalmente el property
s?
Para el punto 5, mi enfoque sería tener un overwrite_prop()
método en la clase base que sobrescriba el atributo de clase actual para que property
ya no se active:
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
Como puede ver, aunque todavía es un poco artificial, es al menos más explícito que un críptico size = None
. Dicho esto, en última instancia, no sobrescribiría la propiedad en absoluto, y reconsideraría mi diseño desde la raíz.
Si has llegado hasta aquí, gracias por caminar este viaje conmigo. Fue un pequeño ejercicio divertido.
len(self.elements)
como implementación. Esta implementación muy específica impone un contrato en todas las instancias de la clase, a saber, que proporcionanelements
cuál es un contenedor de tamaño . Sin embargo, suSquare_Integers_Below
clase no parece comportarse de esa manera (tal vez está generando sus miembros dinámicamente), por lo que debe definir su propio comportamiento.