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

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

Решение

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

Обновить:

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

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

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

__getattr__( self, name)

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

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

Справочник по языку Python:

Чтобы избежать бесконечной рекурсии в этом методе, его реализация всегда должна вызывать метод базового класса с тем же именем для доступа к любым атрибутам, которые ему нужны, например, object.__getattribute__(self, name),

Имея в виду:

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

Вы вызываете атрибут с именем __dict__, Потому что это атрибут, __getattribute__ вызывается в поисках __dict__ какие звонки __getattribute__ который называет... Яда Яда Яда

return  object.__getattribute__(self, name)

Использование базовых классов __getattribute__ помогает найти настоящий атрибут.

Как __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

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

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

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

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

Другие вопросы по тегам