Як мені реалізувати __getattribute__ без нескінченної помилки рекурсії?


101

Я хочу змінити доступ до однієї змінної в класі, але повернути всі інші нормально. Як мені це зробити __getattribute__?

Я спробував наступне (що також має ілюструвати те, що я намагаюся зробити), але я отримую помилку рекурсії:

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

Відповіді:


127

Ви отримуєте помилку рекурсії, оскільки ваша спроба отримати доступ до self.__dict__атрибута всередині __getattribute__викликає ваш __getattribute__знову. Якщо ви використовуєте object's __getattribute__замість цього, він працює:

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)

Це працює тому, що object(у цьому прикладі) є базовим класом. Викликаючи базову версію, __getattribute__ви уникаєте рекурсивного пекла, в якому ви були раніше.

Виведення Ipython з кодом у 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

Оновлення:

Щось у розділі під назвою Більше доступу до атрибутів для класів нового стилю в поточній документації, де вони рекомендують робити саме це, щоб уникнути нескінченної рекурсії.


1
Цікаво. То що ти там робиш? Чому заперечували б мої змінні?
Грег

1
..і я хочу завжди використовувати об'єкт? Що робити, якщо я успадковую від інших класів?
Грег

1
Так, кожного разу, коли ви створюєте клас і не пишете свій власний, ви використовуєте гетатрозподіл, наданий об'єктом.
Егіл

20
хіба не краще використовувати super () і, таким чином, використовувати перший метод getttribute python знаходить у базових класах? -super(D, self).__getattribute__(name)
гепатіно

10
Або ви можете просто скористатися super().__getattribute__(name)в Python 3.
jeromej

25

Власне, я вважаю, що ви хочете скористатися __getattr__спеціальним методом.

Цитата з документів Python:

__getattr__( self, name)

Викликається, коли пошук атрибута не знайшов атрибут у звичайних місцях (тобто він не є атрибутом екземпляра, а також не знайдений у дереві класів для самості). name - ім'я атрибута. Цей метод повинен повернути (обчислене) значення атрибута або підвищити виняток AttributeError.
Зауважте, що якщо атрибут знайдеться через звичайний механізм, __getattr__()він не викликається. (Це навмисна асиметрія між __getattr__()та __setattr__().) Це робиться як з міркувань ефективності, так і тому, що в іншому випадку не __setattr__()було б способу отримати доступ до інших атрибутів екземпляра. Зауважте, що принаймні для змінних примірника ви можете підробити тотальний контроль, не вставляючи жодних значень у словник атрибутів екземпляра (а замість цього вставляючи їх в інший об’єкт). Дивіться__getattribute__() наведений нижче спосіб, щоб реально отримати повний контроль у класах нового стилю.

Примітка: щоб це не працювало, екземпляр не повинен мати testатрибут, тому рядок self.test=20слід видалити.


2
На насправді, в відповідно до характеру коду OP, який замінює __getattr__для testбуло б марно, так як він завжди знаходив , що «в звичайних місцях».
Кейсі Кубалл

1
поточне посилання на відповідні документи Python (схоже, відрізняється від посилань у відповіді): docs.python.org/3/reference/datamodel.html#object.__getattr__
CrepeGoat

17

Довідка мови Python:

Для того , щоб уникнути нескінченної рекурсії в цьому методі, його реалізація завжди повинна викликати метод базового класу з тим же ім'ям доступу атрибути, необхідні, наприклад, object.__getattribute__(self, name).

Значення:

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

Ви викликаєте атрибут, який називається __dict__. Через те, що це атрибут, __getattribute__викликається в пошуках того, __dict__який дзвінок __getattribute__викликає ... yada yada yada

return  object.__getattribute__(self, name)

Використання базових класів __getattribute__допомагає знайти реальний атрибут.


13

Ви впевнені, що хочете використовувати __getattribute__? Чого ви насправді намагаєтесь досягти?

Найпростіший спосіб зробити те, що ви просите, це:

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

    test = 0

або:

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

    @property
    def test(self):
        return 0

Редагувати: Зауважте, що екземпляр у кожному випадку Dматиме різні значення test. У першому випадку d.testбуло б 20, у другому - 0. Я залишу це вам, щоб розібратися, чому.

Edit2: Грег вказав, що приклад 2 не вдасться, оскільки властивість лише для читання, а __init__метод намагався встановити її на 20. Більш повним прикладом цього буде:

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)

Очевидно, що як клас це майже повністю марно, але це дає вам ідею рухатися далі.


О, це не тихо, коли ти ведеш клас, ні? Файл "Script1.py", рядок 5, init self.test = 20 AttributeError: не можна встановити атрибут
Грег

Правда. Я зафіксую це як третій приклад. Добре помічений.
Одномісний

5

Ось більш надійна версія:

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)

Він викликає метод __ getattribute __ з батьківського класу, зрештою потрапляючи назад до об'єкта. __ метод gettttribute __, якщо інші предки не перекривають його.


5

Як застосовується __getattribute__метод?

Він викликається перед звичайним пунктирним пошуком. Якщо вона піднімається AttributeError, то ми дзвонимо __getattr__.

Використання цього методу досить рідкісне. У стандартній бібліотеці є лише два визначення:

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

Найкраща практика

Правильний спосіб програмного керування доступом до одного атрибуту є за допомогою property. Клас Dповинен бути записаний так (за допомогою сеттера та делетера, необов'язково, для копіювання очевидного наміченого поведінки):

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

І використання:

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

Властивість - це дескриптор даних, тому це перше, що потрібно шукати в звичайному алгоритмі крапки пошуку.

Параметри для __getattribute__

У вас є кілька варіантів, якщо вам абсолютно потрібно здійснити пошук для кожного атрибуту через __getattribute__.

  • підвищувати AttributeError, викликаючи __getattr__виклик (якщо він реалізований)
  • повернути щось з нього
    • використовуючи superдля виклику батьківської (можливо object,) реалізації
    • дзвінок __getattr__
    • реалізувати свій власний алгоритм крапкового пошуку

Наприклад:

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

Те, що ви спочатку хотіли.

І цей приклад показує, як ви могли робити те, що спочатку хотіли:

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)

І поведе себе так:

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

Перегляд коду

Ваш код із коментарями. У вас є пунктирний пошук самостійно __getattribute__. Ось чому ви отримуєте помилку рекурсії. Ви можете перевірити, чи є ім'я "__dict__"та використовувати його superдля вирішення, але це не стосується __slots__. Я залишу це як вправу для читача.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.