Como faço para implementar __getattribute__ sem um erro de recursão infinita?

101

Quero substituir o acesso a uma variável em uma classe, mas retornar todas as outras normalmente. Como faço para fazer isso com __getattribute__?

Tentei o seguinte (que também deve ilustrar o que estou tentando fazer), mas recebo um erro de recursão:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp
Greg
fonte

Respostas:

127

Você obtém um erro de recursão porque sua tentativa de acessar o self.__dict__atributo interno __getattribute__invoca seu __getattribute__novamente. Se você usar objecto __getattribute__, funcionará:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return object.__getattribute__(self, name)

Isso funciona porque object(neste exemplo) é a classe base. Ao chamar a versão base de __getattribute__você evita o inferno recursivo em que estava antes.

Saída do Ipython com código em foo.py:

In [1]: from foo import *

In [2]: d = D()

In [3]: d.test
Out[3]: 0.0

In [4]: d.test2
Out[4]: 21

Atualizar:

Há algo na seção intitulada Mais acesso a atributos para classes de novo estilo na documentação atual, onde eles recomendam fazer exatamente isso para evitar a recursão infinita.

Egil
fonte
1
Interessante. Então, o que você está fazendo lá? Por que o objeto teria minhas variáveis?
Greg
1
..e eu quero sempre usar objeto? E se eu herdar de outras classes?
Greg
1
Sim, cada vez que você cria uma classe e não escreve a sua própria, você usa o getattribute fornecido por objeto.
Egil
20
não é melhor usar super () e, portanto, usar o primeiro método getattribute que o python encontra em suas classes base? -super(D, self).__getattribute__(name)
gepatino
10
Ou você pode apenas usar super().__getattribute__(name)em Python 3.
jeromej
25

Na verdade, acredito que você deseja usar o __getattr__método especial.

Citação dos documentos Python:

__getattr__( self, name)

Chamado quando uma pesquisa de atributo não encontrou o atributo nos locais usuais (ou seja, não é um atributo de instância nem foi encontrado na árvore de classe para si mesmo). name é o nome do atributo. Este método deve retornar o valor do atributo (calculado) ou lançar uma exceção AttributeError.
Observe que, se o atributo for encontrado por meio do mecanismo normal, __getattr__()não será chamado. (Esta é uma assimetria intencional entre __getattr__()e __setattr__().) Isso é feito por razões de eficiência e porque, de outra forma __setattr__(), não haveria como acessar outros atributos da instância. Observe que pelo menos para variáveis ​​de instância, você pode fingir controle total não inserindo nenhum valor no dicionário de atributos de instância (mas, em vez disso, inserindo-os em outro objeto). Veja o__getattribute__() método abaixo para obter uma maneira de realmente obter controle total nas classes de novo estilo.

Nota: para que isso funcione, a instância não deve ter um testatributo, então a linha self.test=20deve ser removida.

tzot
fonte
2
Na verdade, de acordo com a natureza do código do OP, substituir __getattr__por testseria inútil, uma vez que sempre o encontraria "nos lugares de costume".
Casey Kuball
1
link atual para documentos Python relevantes (parece ser diferente daquele referenciado na resposta): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat
17

Referência da linguagem Python:

Para evitar recursões infinitas neste método, sua implementação deve sempre chamar o método da classe base com o mesmo nome para acessar quaisquer atributos de que necessite, por exemplo object.__getattribute__(self, name),.

Significado:

def __getattribute__(self,name):
    ...
        return self.__dict__[name]

Você está chamando por um atributo chamado __dict__. Por ser um atributo, __getattribute__é chamado em busca de __dict__quais chamadas __getattribute__que chama ... blá blá blá

return  object.__getattribute__(self, name)

Usar as classes básicas __getattribute__ajuda a encontrar o atributo real.

ttepasse
fonte
13

Tem certeza que deseja usar __getattribute__? O que você está realmente tentando alcançar?

A maneira mais fácil de fazer o que você pede é:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    test = 0

ou:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    @property
    def test(self):
        return 0

Edit: Observe que uma instância de Dteria valores diferentes de testem cada caso. No primeiro caso d.testseria 20, no segundo seria 0. Vou deixar para você descobrir o porquê.

Edit2: Greg apontou que o exemplo 2 falhará porque a propriedade é somente leitura e o __init__método tentou defini-la como 20. Um exemplo mais completo para isso seria:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21

    _test = 0

    def get_test(self):
        return self._test

    def set_test(self, value):
        self._test = value

    test = property(get_test, set_test)

Obviamente, como classe, isso é quase totalmente inútil, mas dá uma ideia para seguir em frente.

Singletoned
fonte
Oh, isso não acalma o trabalho quando você dirige a aula, não? Arquivo "Script1.py", linha 5, em init self.test = 20 AttributeError: não é possível definir o atributo
Greg
Verdade. Vou consertar isso como um terceiro exemplo. Bem localizado.
Solteiro em
5

Aqui está uma versão mais confiável:

class D(object):
    def __init__(self):
        self.test = 20
        self.test2 = 21
    def __getattribute__(self, name):
        if name == 'test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

Ele chama __ getAttribute __ método da classe pai, acabou caindo de volta para objeto. Método __ getattribute __ se outros ancestrais não o substituírem.

ElmoVanKielmo
fonte
5

Como o __getattribute__método é usado?

É chamado antes da pesquisa pontilhada normal. Se aumentar AttributeError, então pagamos __getattr__.

O uso deste método é bastante raro. Existem apenas duas definições na biblioteca padrão:

$ grep -Erl  "def __getattribute__\(self" cpython/Lib | grep -v "/test/"
cpython/Lib/_threading_local.py
cpython/Lib/importlib/util.py

Melhor prática

A maneira correta de controlar programaticamente o acesso a um único atributo é com property. A classe Ddeve ser escrita da seguinte forma (com o setter e o deleter opcionalmente para replicar o comportamento pretendido aparente):

class D(object):
    def __init__(self):
        self.test2=21

    @property
    def test(self):
        return 0.

    @test.setter
    def test(self, value):
        '''dummy function to avoid AttributeError on setting property'''

    @test.deleter
    def test(self):
        '''dummy function to avoid AttributeError on deleting property'''

E uso:

>>> o = D()
>>> o.test
0.0
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0

Uma propriedade é um descritor de dados, portanto, é a primeira coisa procurada no algoritmo normal de pesquisa pontilhada.

Opções para __getattribute__

Você terá várias opções se for absolutamente necessário implementar a pesquisa para cada atributo via __getattribute__.

  • aumentar AttributeError, fazendo __getattr__com que seja chamado (se implementado)
  • devolver algo dele por
    • usando superpara chamar a objectimplementação pai (provavelmente )
    • chamando __getattr__
    • implementar seu próprio algoritmo de pesquisa pontilhada de alguma forma

Por exemplo:

class NoisyAttributes(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self, name):
        print('getting: ' + name)
        try:
            return super(NoisyAttributes, self).__getattribute__(name)
        except AttributeError:
            print('oh no, AttributeError caught and reraising')
            raise
    def __getattr__(self, name):
        """Called if __getattribute__ raises AttributeError"""
        return 'close but no ' + name    


>>> n = NoisyAttributes()
>>> nfoo = n.foo
getting: foo
oh no, AttributeError caught and reraising
>>> nfoo
'close but no foo'
>>> n.test
getting: test
20

O que você queria originalmente.

E este exemplo mostra como você pode fazer o que queria originalmente:

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:
            return super(D, self).__getattribute__(name)

E vai se comportar assim:

>>> o = D()
>>> o.test = 'foo'
>>> o.test
0.0
>>> del o.test
>>> o.test
0.0
>>> del o.test

Traceback (most recent call last):
  File "<pyshell#216>", line 1, in <module>
    del o.test
AttributeError: test

Revisão de código

Seu código com comentários. Você tem uma pesquisa pontilhada sobre si mesmo em __getattribute__. É por isso que você obtém um erro de recursão. Você pode verificar se o nome é "__dict__"e usar superpara contornar o problema, mas isso não cobre __slots__. Vou deixar isso como um exercício para o leitor.

class D(object):
    def __init__(self):
        self.test=20
        self.test2=21
    def __getattribute__(self,name):
        if name=='test':
            return 0.
        else:      #   v--- Dotted lookup on self in __getattribute__
            return self.__dict__[name]

>>> print D().test
0.0
>>> print D().test2
...
RuntimeError: maximum recursion depth exceeded in cmp
Aaron Hall
fonte