Como acessar os atributos de classe de uma superclass no Python?
-
25-09-2019 - |
Pergunta
Dê uma olhada no seguinte código:
class A(object):
defaults = {'a': 1}
def __getattr__(self, name):
print('A.__getattr__')
return self.get_default(name)
@classmethod
def get_default(cls, name):
# some debug output
print('A.get_default({}) - {}'.format(name, cls))
try:
print(super(cls, cls).defaults) # as expected
except AttributeError: #except for the base object class, of course
pass
# the actual function body
try:
return cls.defaults[name]
except KeyError:
return super(cls, cls).get_default(name) # infinite recursion
#return cls.__mro__[1].get_default(name) # this works, though
class B(A):
defaults = {'b': 2}
class C(B):
defaults = {'c': 3}
c = C()
print('c.a =', c.a)
Eu tenho uma hierarquia de classes, cada uma com seu próprio dicionário contendo alguns valores padrão. Se uma instância de uma classe não tiver um atributo específico, um valor padrão deve ser retornado. Se nenhum valor padrão para o atributo estiver contido na classe atual defaults
dicionário, a superclasse defaults
O dicionário deve ser pesquisado.
Estou tentando implementar isso usando o método de classe recursiva get_default
. O programa fica preso em uma recursão infinita, infelizmente. Minha compreensão de super()
obviamente está faltando. Acessando __mro__
, Posso fazer com que funcione corretamente, mas não tenho certeza se essa é uma solução adequada.
Eu tenho a sensação de que a resposta está em algum lugar em Este artigo, mas ainda não consegui encontrá -lo. Talvez eu precise recorrer ao uso de um metaclasse?
editar: Na minha aplicação, __getattr__
Primeiros cheques self.base
. Se não é None
, o atributo precisa ser buscado a partir daí. Somente no outro caso, um valor padrão deve ser retornado. Eu provavelmente poderia substituir __getattribute__
. Essa seria a melhor solução?
Editar 2: Abaixo está um exemplo estendido da funcionalidade que estou procurando. Atualmente é implementado usando __mro__
(A sugestão anterior da Unutbu, em oposição ao meu método recursivo original). A menos que alguém possa sugerir uma solução mais elegante, fico feliz em usar essa implementação. Eu espero que isso esclareça as coisas.
class A(object):
defaults = {'a': 1}
def __init__(self, name, base=None):
self.name = name
self.base = base
def __repr__(self):
return self.name
def __getattr__(self, name):
print(" '{}' attribute not present in '{}'".format(name, self))
if self.base is not None:
print(" getting '{}' from base ({})".format(name, self.base))
return getattr(self.base, name)
else:
print(" base = None; returning default value")
return self.get_default(name)
def get_default(self, name):
for cls in self.__class__.__mro__:
try:
return cls.defaults[name]
except KeyError:
pass
raise KeyError
class B(A):
defaults = {'b': 2}
class C(B):
defaults = {'c': 3}
c1 = C('c1')
c1.b = 55
print('c1.a = ...'); print(' ...', c1.a) # 1
print(); print('c1.b = ...'); print(' ...', c1.b) # 55
print(); print('c1.c = ...'); print(' ...', c1.c) # 3
c2 = C('c2', base=c1)
c2.c = 99
print(); print('c2.a = ...'); print(' ...', c2.a) # 1
print(); print('c2.b = ...'); print(' ...', c2.b) # 55
print(); print('c2.c = ...'); print(' ...', c2.c) # 99
A saída:
c1.a = ...
'a' attribute not present in 'c1'
base = None; returning default value
... 1
c1.b = ...
... 55
c1.c = ...
'c' attribute not present in 'c1'
base = None; returning default value
... 3
c2.a = ...
'a' attribute not present in 'c2'
getting 'a' from base (c1)
'a' attribute not present in 'c1'
base = None; returning default value
... 1
c2.b = ...
'b' attribute not present in 'c2'
getting 'b' from base (c1)
... 55
c2.c = ...
... 99
Solução 6
A solução proposta na segunda edição da pergunta ainda é a única que fornece tudo o que meu aplicativo exige. Embora o código da Unutbu possa ser mais simples de entender, o __mro__
A solução fornece algumas vantagens IMO (consulte os comentários).
Outras dicas
Não é realmente uma resposta, mas uma observação:
Isso me parece exagerado para mim, uma armadilha comum ao procurar desculpas para usar a magia do Python.
Se você pode se incomodar em definir um defaults
Dict para uma aula, por que não definir apenas os atributos? o efeito é o mesmo.
class A:
a = 1
class B(A):
b = 2
class C(B):
c = 3
c = C()
print('c.a =', c.a)
EDITAR:
Quanto a responder à pergunta, eu provavelmente usaria __getattribute__
em combinação com minha sugestão como esta:
def __getattribute__(self, name):
try:
return object.__getattribute__(self.base, name)
except AttributeError:
return object.__getattribute__(self, name)
Eu acho que o problema resulta de mal -entendidos do propósito de super()
.
http://docs.python.org/library/functions.html#super
Essencialmente, envolver seu objeto (ou classe) em Super () faz com que o Python pule a classe mais recentemente inetçada ao fazer a pesquisa de atributos. No seu código, isso resulta na ignição da classe C ao procurar get_default, mas isso realmente não faz nada, já que C não define um get_default de qualquer maneira. Naturalmente, isso resulta em um loop infinito.
A solução é definir essa função em cada classe que deriva de A. Isso pode ser feito usando um metaclasse:
class DefaultsClass(type):
def __init__(cls, name, bases, dct):
def get_default(self, name):
# some debug output
print('A.get_default(%s) - %s' % (name, cls))
try:
print(cls.defaults) # as expected
except AttributeError: #except for the base object class, of course
pass
# the actual function body
try:
return cls.defaults[name]
except KeyError:
return super(cls, self).get_default(name) # cooperative superclass
cls.get_default = get_default
return super(DefaultsClass, cls).__init__(name, bases, dct)
class A(object):
defaults = {'a': 1}
__metaclass__ = DefaultsClass
def __getattr__(self, name):
return self.get_default(name)
class B(A):
defaults = {'b': 2}
class C(B):
defaults = {'c': 3}
c = C()
print('c.a =', c.a)
print('c.b =', c.b)
print('c.c =', c.c)
resultados:
A.get_default(c) - <class '__main__.C'>
{'c': 3}
('c.c =', 3)
A.get_default(b) - <class '__main__.C'>
{'c': 3}
A.get_default(b) - <class '__main__.B'>
{'b': 2}
('c.b =', 2)
A.get_default(a) - <class '__main__.C'>
{'c': 3}
A.get_default(a) - <class '__main__.B'>
{'b': 2}
A.get_default(a) - <class '__main__.A'>
{'a': 1}
('c.a =', 1)
Devo notar que a maioria das pessoas do Python considerará isso uma solução muito bizarra, e você deve usar isso apenas se você verdade Precisa, talvez para apoiar o código legado.
Para ser mais claro sobre o seu caso "Base" vs "padrão".
>>> class A(object):
... a = 1
...
>>> class B(A):
... b = 2
...
>>> class C(B):
... c = 3
...
>>> a = A()
>>> b = B()
>>> c = C()
>>>
>>> b.b = 23
>>> b.a
1
>>> b.b
23
>>> c.a
1
>>> c.b
2
>>> c.c
3
>>> c.c = 45
>>> c.c
45
Isso cobre o seu caso de uso declarado. Você não precisa de magia. Se a sua USECASE for um pouco diferente, explique o que é e vamos lhe dizer como fazer isso sem mágica. ;)
Que tal:
class A(object):
def __init__(self,base=None):
self.a=1
if base is not None:
self.set_base(base)
super(A,self).__init__()
def set_base(self,base):
for key in ('a b c'.split()):
setattr(self,key,getattr(base,key))
class B(A):
def __init__(self,base=None):
self.b=2
super(B,self).__init__(base)
class C(B):
def __init__(self,base=None):
self.c=3
super(C,self).__init__(base)
c1=C()
c1.b=55
print(c1.a)
print(c1.b)
print(c1.c)
# 1
# 55
# 3
c2=C(c1)
c2.c=99
print(c2.a)
print(c2.b)
print(c2.c)
# 1
# 55
# 99
c1.set_base(c2)
print(c1.a)
print(c1.b)
print(c1.c)
# 1
# 55
# 99
Você deveria usar Nome Mangling aqui.
Renomear defaults
para __defaults
Isso dá a cada classe um atributo inequívoco para que eles não sejam confundidos um com o outro