Esta será uma resposta prolongada que pode servir apenas como cortesia ... mas sua pergunta me levou para um passeio pela toca do coelho, então eu gostaria de compartilhar minhas descobertas (e dor) também.
Você pode achar que essa resposta não é útil para o seu problema real. De fato, minha conclusão é que - eu não faria isso. Dito isto, o pano de fundo desta conclusão pode entretê-lo um pouco, pois você está procurando mais detalhes.
Abordando alguns equívocos
A primeira resposta, embora correta na maioria dos casos, nem sempre é o caso. Por exemplo, considere esta 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
, enquanto sendo um property
, é irrevogavelmente um atributo de instância:
>>> 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!'
Tudo depende de onde você property
está definido em primeiro lugar. Se seu @property
é definido dentro da classe "escopo" (ou realmente, o namespace
), ele se torna um atributo de classe. No meu exemplo, a classe em si não tem conhecimento inst_prop
até ser instanciada. Obviamente, não é muito útil como propriedade aqui.
Mas primeiro, vamos abordar seu comentário sobre a resolução de herança ...
Então, como exatamente a herança influencia esse problema? Este artigo a seguir mergulha um pouco no tópico e a Ordem de resolução do método está um pouco relacionada, embora discuta principalmente a amplitude da herança em vez da profundidade.
Combinado com a nossa descoberta, dadas as configurações abaixo:
@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 o que acontece quando essas linhas são executadas:
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}')
O resultado é assim:
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>
Note como:
self.world_view
foi capaz de ser aplicado, enquanto self.culture
falhou
culture
não existe em Child.__dict__
(o mappingproxy
da classe, não deve ser confundido com a instância __dict__
)
- Mesmo que
culture
exista c.__dict__
, não é referenciado.
Você pode adivinhar o porquê - world_view
foi substituído pela Parent
classe como uma não propriedade, e também Child
foi possível substituí-lo. Enquanto isso, como culture
é herdado, ele existe apenas dentro mappingproxy
deGrandparent
:
Grandparent.__dict__ is: {
'__module__': '__main__',
'culture': <property object at 0x00694C00>,
'world_view': <property object at 0x00694C00>,
...
}
De fato, se você tentar remover Parent.culture
:
>>> del Parent.culture
Traceback (most recent call last):
File "<pyshell#67>", line 1, in <module>
del Parent.culture
AttributeError: culture
Você notará que nem existe Parent
. Porque o objeto está se referindo diretamente novamente Grandparent.culture
.
Então, e a ordem de resolução?
Portanto, estamos interessados em observar a ordem de resolução real, vamos tentar remover 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}')
Gostaria de saber qual é o resultado?
c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>
Ele voltou ao Grandparent's world_view
property
, apesar de termos conseguido atribuir o self.world_view
antes! Mas e se mudarmos vigorosamente world_view
no nível de classe, como a outra resposta? E se a excluirmos? E se atribuirmos o atributo de classe atual a uma propriedade?
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}')
O resultado é:
# 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>
Isso é interessante porque c.world_view
é restaurado ao seu atributo de instância, enquanto Child.world_view
é o que atribuímos . Após remover o atributo de instância, ele reverte para o atributo de classe. E depois de reatribuir a Child.world_view
propriedade à, perdemos instantaneamente o acesso ao atributo de instância.
Portanto, podemos supor a seguinte ordem de resolução :
- Se um atributo de classe existir e for a
property
, recupere seu valor via getter
ou fget
(mais sobre isso mais adiante). Classe atual primeiro da classe Base por último.
- Caso contrário, se existir um atributo de instância, recupere o valor do atributo de instância.
- Senão, recupere o
property
atributo que não é de classe. Classe atual primeiro da classe Base por último.
Nesse caso, vamos remover a raiz property
:
del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')
Que 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'
Surpresa! Child
agora tem seus próprios com culture
base na inserção forçada em c.__dict__
. Child.culture
não existe, é claro, uma vez que nunca foi definido em Parent
ou Child
atributo de classe e Grandparent
foi removido.
Essa é a causa raiz do meu problema?
Na verdade não . O erro que você está recebendo, que ainda estamos observando ao atribuir self.culture
, é totalmente diferente . Mas a ordem de herança define o pano de fundo para a resposta - que é a property
própria.
Além do getter
método mencionado anteriormente , property
também tem alguns truques legais nas mangas. O mais relevante nesse caso é o método, setter
ou fset
, que é acionado por self.culture = ...
linha. Como você property
não implementou nenhuma função setter
ou fget
, python não sabe o que fazer e lança um AttributeError
(ie can't set attribute
).
Se, no entanto, você implementou um 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...
Ao instanciar a Child
classe, você receberá:
Instantiating Child class...
property setter is called!
Em vez de receber um AttributeError
, agora você está realmente chamando o some_prop.setter
método O que lhe dá mais controle sobre seu objeto ... com nossas descobertas anteriores, sabemos que precisamos ter um atributo de classe substituído antes que ele atinja a propriedade. Isso pode ser implementado dentro da classe base como um gatilho. Aqui está um novo exemplo:
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)
O que resulta em:
Fine, have your own culture
I'm a millennial!
SURPRESA! Agora você pode substituir seu próprio atributo de instância em uma propriedade herdada!
Então, problema resolvido?
... Na verdade não. O problema com essa abordagem é que agora você não pode ter um setter
método adequado . Há casos em que você deseja definir valores no seu property
. Mas agora, sempre que você definir self.culture = ...
, sempre substituirá qualquer função que você definiu na getter
(que neste caso é realmente apenas a @property
parte envolvida. Você pode adicionar medidas mais sutis, mas de uma forma ou de outra sempre envolverá mais do que apenas self.culture = ...
. por exemplo:
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
É muito mais complicado que a outra resposta, size = None
no nível da classe.
Você também pode escrever seu próprio descritor para manipular os métodos __get__
e __set__
ou adicionais. Mas no final do dia, quando self.culture
é referenciada, __get__
sempre será acionada primeiro e, quando self.culture = ...
é referenciada, __set__
sempre será acionada primeiro. Não há como contornar isso, tanto quanto eu tentei.
O cerne da questão, IMO
O problema que vejo aqui é - você não pode comer o seu bolo e também. property
significa um descritor com acesso conveniente a partir de métodos como getattr
ou setattr
. Se você também deseja que esses métodos atinjam um objetivo diferente, está apenas pedindo problemas. Talvez eu repensasse a abordagem:
- Eu realmente preciso de um
property
para isso?
- Um método poderia me servir de maneira diferente?
- Se eu precisar de
property
, existe algum motivo para substituí-lo?
- A subclasse realmente pertence à mesma família se
property
não se aplicar?
- Se eu precisar sobrescrever alguns / todos os
property
s, um método separado me serviria melhor do que simplesmente reatribuir, pois a reatribuição pode acidentalmente anular os property
s?
Para o ponto 5, minha abordagem seria ter um overwrite_prop()
método na classe base que substituísse o atributo de classe atual para que property
não seja mais acionado:
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 você pode ver, embora ainda seja um pouco artificial, é pelo menos mais explícito que um enigmático size = None
. Dito isso, em última análise, eu não sobrescreveria a propriedade e reconsideraria meu projeto da raiz.
Se você chegou até aqui - obrigado por percorrer essa jornada comigo. Foi um pequeno exercício divertido.