Как реализовать __getattribute__ без бесконечной ошибки рекурсии?

102

Я хочу переопределить доступ к одной переменной в классе, но вернуть все остальные в обычном режиме. Как мне это сделать __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
Грег
источник

Ответы:

128

Вы получаете ошибку рекурсии, потому что ваша попытка получить доступ к 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
Да, каждый раз, когда вы создаете класс и не пишете свой собственный, вы используете атрибут get, предоставляемый объектом.
Эгиль
20
не лучше ли использовать super () и, таким образом, использовать первый метод getattribute, который python находит в ваших базовых классах? -super(D, self).__getattribute__(name)
гепатино 07
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__for 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__помогает найти настоящий атрибут.

ttepasse
источник
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: невозможно установить атрибут
Грег
Правда. Я исправлю это как третий пример. Хорошо подмечено.
Singletoned
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 __ из родительского класса, в конечном итоге возвращаясь к объекту. __ getattribute __, если другие предки не отменяют его.

ElmoVanKielmo
источник
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__.

  • Raise 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
Аарон Холл
источник