Jak zaimplementować __getattribute__ bez nieskończonego błędu rekurencji?

102

Chcę przesłonić dostęp do jednej zmiennej w klasie, ale normalnie zwrócić wszystkie inne. Jak to osiągnąć za pomocą__getattribute__ ?

Wypróbowałem następujące rozwiązania (które powinny również zilustrować, co próbuję zrobić), ale pojawia się błąd rekursji:

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
źródło

Odpowiedzi:

128

Otrzymujesz błąd rekursji, ponieważ próba uzyskania dostępu do self.__dict__atrybutu wewnątrz __getattribute__wywołuje __getattribute__ponownie. Jeśli zamiast tego użyjesz object's __getattribute__, zadziała:

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)

To działa, ponieważ object(w tym przykładzie) jest klasą bazową. Wywołując podstawową wersję __getattribute__siebie, unikaj rekurencyjnego piekła, w którym byłeś wcześniej.

Wyjście Ipython z kodem w 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

Aktualizacja:

Jest coś w sekcji zatytułowanej Więcej dostępu do atrybutów dla klas nowego stylu w aktualnej dokumentacji, gdzie zalecają zrobienie dokładnie tego, aby uniknąć nieskończonej rekurencji.

Egil
źródło
1
Ciekawy. Więc co ty tam robisz? Dlaczego obiekt miałby mieć moje zmienne?
Greg
1
..i zawsze chcę używać obiektu? A jeśli odziedziczę z innych klas?
Greg
1
Tak, za każdym razem, gdy tworzysz klasę i nie piszesz własnej, używasz getattribute dostarczonego przez obiekt.
Egil
20
czy nie lepiej jest użyć super (), a tym samym użyć pierwszej metody getattribute, którą Python znajdzie w twoich klasach bazowych? -super(D, self).__getattribute__(name)
gepatino
10
Lub możesz po prostu użyć super().__getattribute__(name)w Pythonie 3.
jeromej
25

Właściwie uważam, że __getattr__zamiast tego chcesz użyć specjalnej metody.

Cytat z dokumentacji Pythona:

__getattr__( self, name)

Wywoływane, gdy wyszukiwanie atrybutu nie znalazło atrybutu w zwykłych miejscach (tj. Nie jest atrybutem instancji ani nie można go znaleźć w drzewie klas dla siebie). nazwa to nazwa atrybutu. Ta metoda powinna zwrócić (obliczoną) wartość atrybutu lub zgłosić wyjątek AttributeError.
Zauważ, że jeśli atrybut zostanie znaleziony za pomocą normalnego mechanizmu, __getattr__()nie jest wywoływany. (Jest to celowa asymetria między __getattr__()i metodą poniżej, aby faktycznie uzyskać całkowitą kontrolę w klasach nowego stylu.__setattr__() .) Jest to robione zarówno ze względu na wydajność, jak i dlatego, że w przeciwnym razie nie __setattr__()byłoby możliwości uzyskania dostępu do innych atrybutów instancji. Zauważ, że przynajmniej w przypadku zmiennych instancji możesz udawać całkowitą kontrolę, nie wstawiając żadnych wartości do słownika atrybutów instancji (ale zamiast tego wstawiając je do innego obiektu). Zobacz__getattribute__()

Uwaga: aby to zadziałało, instancja nie powinna mieć testatrybutu, więc wierszself.test=20 należy usunąć.

tzot
źródło
2
W rzeczywistości, zgodnie z naturą kodu PO, nadpisywanie __getattr__dla testbyłoby bezużyteczne, ponieważ zawsze znajdowałoby się ono „w zwykłych miejscach”.
Casey Kuball
1
aktualny link do odpowiednich dokumentów Pythona (wydaje się, że różni się od tego, do którego odwołuje się odpowiedź): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat
17

Dokumentacja języka Python:

Aby uniknąć nieskończonej rekursji w tej metodzie, jej implementacja powinna zawsze wywoływać metodę klasy bazowej o tej samej nazwie, aby uzyskać dostęp do potrzebnych atrybutów, na przykład object.__getattribute__(self, name).

Znaczenie:

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

Wzywasz atrybut o nazwie __dict__. Ponieważ jest to atrybut, __getattribute__zostanie wywołany w poszukiwaniu __dict__który domaga __getattribute__który wzywa ... bla bla bla

return  object.__getattribute__(self, name)

Korzystanie z klas bazowych __getattribute__pomaga znaleźć prawdziwy atrybut.

ttepasse
źródło
13

Czy na pewno chcesz użyć __getattribute__? Co tak naprawdę próbujesz osiągnąć?

Najłatwiej zrobić to, o co prosisz:

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

    test = 0

lub:

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

    @property
    def test(self):
        return 0

Edycja: Zwróć uwagę, że instancja Dbędzie miała różne wartości testw każdym przypadku. W pierwszym przypadku d.testbędzie to 20, w drugim 0. Zostawię ci wyjaśnienie, dlaczego.

Edit2: Greg zwrócił uwagę, że przykład 2 zakończy się niepowodzeniem, ponieważ właściwość jest tylko do odczytu, a __init__metoda próbowała ustawić ją na 20. Bardziej kompletnym przykładem byłoby:

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)

Oczywiście jako klasa jest to prawie całkowicie bezużyteczne, ale daje ci pomysł, od którego możesz odejść.

Singletoned
źródło
Ale to nie działa po cichu, kiedy prowadzisz zajęcia, prawda? Plik „Script1.py”, wiersz 5, init self.test = 20 AttributeError: nie można ustawić atrybutu
Greg,
Prawdziwe. Poprawię to jako trzeci przykład. Dobrze zauważony.
Singletoned
5

Oto bardziej niezawodna wersja:

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)

To nazywa __ getAttribute __ metodę z klasy nadrzędnej, w końcu spada z powrotem do obiektu. __ getattribute __ , jeśli inni przodkowie jej nie zastępują.

ElmoVanKielmo
źródło
5

Jak jest __getattribute__używana metoda?

Jest wywoływana przed zwykłym wyszukiwaniem kropkowanym. Jeśli wzrośnie AttributeError, dzwonimy__getattr__ .

Stosowanie tej metody jest raczej rzadkie. W bibliotece standardowej są tylko dwie definicje:

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

Najlepsze praktyki

Właściwym sposobem programowego kontrolowania dostępu do pojedynczego atrybutu jest property. Klasa Dpowinna być zapisana w następujący sposób (z ustawieniem i usunięciem opcjonalnie w celu odtworzenia widocznego zamierzonego zachowania):

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

I użycie:

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

Właściwość jest deskryptorem danych, dlatego jest pierwszą rzeczą, której szuka się w normalnym algorytmie wyszukiwania kropkowego.

Opcje dla __getattribute__

Masz kilka opcji, jeśli absolutnie musisz zaimplementować wyszukiwanie dla każdego atrybutu za pośrednictwem __getattribute__.

  • podnieść AttributeError, powodując __getattr__wywołanie (jeśli zaimplementowano)
  • zwrócić coś z tego przez
    • za pomocą superwywołania nadrzędnej (prawdopodobnie object) implementacji
    • powołanie __getattr__
    • zaimplementowanie w jakiś sposób własnego algorytmu wyszukiwania kropkowego

Na przykład:

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

To, czego pierwotnie chciałeś.

Ten przykład pokazuje, jak możesz zrobić to, czego pierwotnie chciałeś:

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)

I będzie się tak zachowywać:

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

Przegląd kodu

Twój kod z komentarzami. Masz wykropkowane wyszukiwanie siebie w __getattribute__. Dlatego pojawia się błąd rekursji. Możesz sprawdzić, czy nazwa jest "__dict__"i użyć superdo obejścia, ale to nie obejmuje __slots__. Zostawię to jako ćwiczenie dla czytelnika.

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
źródło