Как реализовать __getattribute__ без бесконечной ошибки рекурсии? - PullRequest
80 голосов
/ 16 декабря 2008

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

Ответы [ 6 ]

108 голосов
/ 16 декабря 2008

Вы получаете ошибку рекурсии, потому что ваша попытка получить доступ к атрибуту 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

Обновление:

Что-то есть в разделе под названием Больше доступа к атрибутам для классов нового стиля в текущей документации, где они рекомендуют делать именно это, чтобы избежать бесконечной рекурсии.

22 голосов
/ 17 декабря 2008

На самом деле, я считаю, что вы хотите использовать специальный метод __getattr__.

Цитата из документации по Python:

__getattr__( self, name)

Вызывается, когда поиск атрибута не нашел атрибут в обычных местах (то есть он не является атрибутом экземпляра и не найден в дереве классов для себя). имя это имя атрибута. Этот метод должен вернуть (вычисленное) значение атрибута или вызвать исключение AttributeError.
Обратите внимание, что если атрибут найден через обычный механизм, __getattr__() не вызывается. (Это намеренная асимметрия между __getattr__() и __setattr__().) Это делается как по соображениям эффективности, так и в противном случае __setattr__() не будет иметь доступа к другим атрибутам экземпляра. Обратите внимание, что по крайней мере для переменных экземпляра вы можете подделать тотальный контроль, не вставляя никаких значений в словарь атрибутов экземпляра (вместо этого вставляя их в другой объект). См. Метод __getattribute__() ниже, чтобы узнать, как на самом деле получить полный контроль в классах нового стиля.

Примечание: чтобы это работало, экземпляр должен не иметь атрибут test, поэтому следует удалить строку self.test=20.

15 голосов
/ 16 декабря 2008

Справочник по языку 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 голосов
/ 16 декабря 2008

Вы уверены, что хотите использовать __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

Edit: Обратите внимание, что экземпляр 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)

Очевидно, что для класса это почти совершенно бесполезно, но дает вам идею двигаться дальше.

5 голосов
/ 29 мая 2013

Вот более надежная версия:

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 __ метод, если другие предки не отменяйте это.

3 голосов
/ 06 февраля 2015

Как используется метод __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
...