Come implemento __getattribute__ senza un errore di ricorsione infinito?


101

Voglio sovrascrivere l'accesso a una variabile in una classe, ma restituire normalmente tutte le altre. Come posso farlo con __getattribute__?

Ho provato quanto segue (che dovrebbe anche illustrare cosa sto cercando di fare) ma ottengo un errore di ricorsione:

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

Risposte:


127

Ricevi un errore di ricorsione perché il tuo tentativo di accedere self.__dict__all'attributo all'interno __getattribute__richiama di __getattribute__nuovo il tuo . Se si utilizza object's __getattribute__, invece, funziona:

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)

Questo funziona perché object(in questo esempio) è la classe base. Chiamando la versione base di __getattribute__te eviti l'inferno ricorsivo in cui ti trovavi prima.

Output Ipython con codice in 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

Aggiornare:

C'è qualcosa nella sezione intitolata Più accesso agli attributi per le classi di nuovo stile nella documentazione corrente, dove si consiglia di fare esattamente questo per evitare la ricorsione infinita.


1
Interessante. Allora cosa ci fai lì? Perché l'oggetto dovrebbe avere le mie variabili?
Greg

1
..e voglio usare sempre object? E se eredito da altre classi?
Greg

1
Sì, ogni volta che crei una classe e non ne scrivi una tua, usi il getattribute fornito da object.
Egil

20
non è meglio usare super () e quindi usare il primo metodo getattribute che python trova nelle tue classi base? -super(D, self).__getattribute__(name)
gepatino

10
Oppure puoi semplicemente usarlo super().__getattribute__(name)in Python 3.
jeromej

25

In realtà, credo che tu voglia utilizzare __getattr__invece il metodo speciale.

Citazione dalla documentazione di Python:

__getattr__( self, name)

Chiamato quando una ricerca di attributi non ha trovato l'attributo nelle posizioni usuali (cioè non è un attributo di istanza né si trova nell'albero delle classi per self). nome è il nome dell'attributo. Questo metodo dovrebbe restituire il valore dell'attributo (calcolato) o sollevare un'eccezione AttributeError.
Notare che se l'attributo viene trovato tramite il normale meccanismo, __getattr__()non viene chiamato. (Questa è un'asimmetria intenzionale tra __getattr__()e __setattr__().) Questo viene fatto sia per ragioni di efficienza sia perché altrimenti non __setattr__()avrebbe modo di accedere ad altri attributi dell'istanza. Nota che almeno per le variabili di istanza, puoi simulare il controllo totale non inserendo alcun valore nel dizionario degli attributi di istanza (ma inserendoli invece in un altro oggetto). Vedi il__getattribute__() metodo di seguito per un modo per ottenere effettivamente il controllo totale nelle classi di nuovo stile.

Nota: affinché funzioni, l'istanza non dovrebbe avere un testattributo, quindi la riga self.test=20dovrebbe essere rimossa.


2
In realtà, a seconda della natura del codice dell'OP, l'override __getattr__di testsarebbe inutile, poiché lo troverebbe sempre "nei posti soliti".
Casey Kuball,

1
collegamento corrente ai documenti Python pertinenti (sembra essere diverso da quello a cui si fa riferimento nella risposta): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat

17

Riferimento al linguaggio Python:

Per evitare la ricorsione infinita in questo metodo, la sua implementazione dovrebbe sempre chiamare il metodo della classe base con lo stesso nome per accedere a tutti gli attributi di cui ha bisogno, ad esempio object.__getattribute__(self, name),.

Senso:

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

Stai chiamando per un attributo chiamato __dict__. Poiché è un attributo, __getattribute__viene chiamato alla ricerca di __dict__quali chiamate __getattribute__che chiama ... yada yada yada

return  object.__getattribute__(self, name)

L'uso delle classi base __getattribute__aiuta a trovare l'attributo reale.


13

Sei sicuro di volerlo usare __getattribute__? Cosa stai effettivamente cercando di ottenere?

Il modo più semplice per fare ciò che chiedi è:

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

    test = 0

o:

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

    @property
    def test(self):
        return 0

Modifica: nota che un'istanza di Davrebbe valori diversi di testin ogni caso. Nel primo caso d.testsarebbe 20, nel secondo sarebbe 0. Lascio a te capire perché.

Edit2: Greg ha sottolineato che l'esempio 2 fallirà perché la proprietà è di sola lettura e il __init__metodo ha cercato di impostarla su 20. Un esempio più completo potrebbe essere:

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)

Ovviamente, come classe questo è quasi del tutto inutile, ma ti dà un'idea da cui andare avanti.


Oh, questo non funziona silenziosamente quando gestisci la classe, no? File "Script1.py", riga 5, in init self.test = 20 AttributeError: impossibile impostare l'attributo
Greg

Vero. Lo sistemerò come terzo esempio. Ben individuato.
Singletoned

5

Ecco una versione più affidabile:

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)

Chiama il metodo __ getattribute __ dalla classe genitore, ricadendo infine sull'oggetto. __ getattribute __ se altri antenati non lo sovrascrivono.


5

Come viene __getattribute__utilizzato il metodo?

Viene chiamato prima della normale ricerca punteggiata. Se aumenta AttributeError, allora chiamiamo __getattr__.

L'uso di questo metodo è piuttosto raro. Ci sono solo due definizioni nella libreria standard:

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

La migliore pratica

Il modo corretto per controllare a livello di codice l'accesso a un singolo attributo è con property. La classe Ddovrebbe essere scritta come segue (con setter e deleter opzionalmente per replicare l'apparente comportamento previsto):

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 utilizzo:

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

Una proprietà è un descrittore di dati, quindi è la prima cosa da cercare nel normale algoritmo di ricerca puntata.

Opzioni per __getattribute__

Hai diverse opzioni se devi assolutamente implementare la ricerca di ogni attributo tramite __getattribute__.

  • rilanciare AttributeError, provocando la __getattr__chiamata (se implementata)
  • restituire qualcosa da esso entro
    • usando superper chiamare l' objectimplementazione genitore (probabilmente )
    • chiamando __getattr__
    • implementando in qualche modo il tuo algoritmo di ricerca puntato

Per esempio:

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

Quello che volevi originariamente.

E questo esempio mostra come potresti fare ciò che volevi originariamente:

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 si comporterà così:

>>> 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

Revisione del codice

Il tuo codice con commenti. Hai una ricerca puntata su se stesso in __getattribute__. Questo è il motivo per cui ottieni un errore di ricorsione. È possibile verificare se il nome è "__dict__"e utilizzare superper risolvere il problema, ma questo non copre __slots__. Lo lascio come esercizio al lettore.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.