Сочетание __setattr__ и __getattr__ вызывает бесконечный цикл

Я пытаюсь построить систему, в которой базовый класс используется для каждого другого объекта. Каждый базовый объект имеет внутренний словарь _fields, в котором реализации базового класса могут хранить свою информацию.

Реализация базового класса довольно проста:

class A(object):
    def __init__(self, fields=dict()):
        self._fields = fields

Реализация класса может установить поле в вызове __init__ в его super().

Я хотел бы добавить, что поля доступны как свойства без добавления декоратора @property к целому набору функций. Для этой цели я переопределил __getattr__ в базовом классе следующим образом:

class A(object):
    def __init__(self, fields=dict()):
        self._fields = fields

    def __getattr__(self, name):
        if hasattr(self, name):
            return object.__getattribute__(self, name)
        elif name in self._fields:
            return self._fields.get(name)
        else:
            raise AttributeError

Теперь реализация для этого класса может работать так:

class A_impl(A):
    def __init__(self):
        super(A_impl, self).__init__(
                    fields=dict(
                    val_1="foo",
                    val_2="",
                    val_3="bar",
                )
            )

Благодаря чему создание реализации этого класса дает вам следующие возможности:

test = A_imp()
print test.val_1
print test.val_2
print test.val_3

Что возвращает

foo

bar

Я даже могу переопределить это с помощью декораторов @property, изменив класс следующим образом:

class A_impl(A):
    def __init__(self):
        super(A_impl, self).__init__(
                    fields=dict(
                    val_1="foo",
                    val_2="",
                    val_3="bar",
                )
            )

    @property
    def val_1(self):
        return self._fields.get('val_1') + "_getter"

Что позволяет мне манипулировать данными для возврата. Единственная проблема заключается в том, что если я хочу иметь возможность установить одну из этих переменных поля, я должен реализовать функции установки дескриптора, что также требует от меня создания дескриптора свойства, который создает много дублирующей работы ( т.е. я должен определить дескрипторы для ВСЕХ моих полей, чего я хочу избежать здесь.

Я реализовал функцию __setattr__ для базового класса, чтобы решить проблему, из-за которой, если я вручную реализую функцию установки дескриптора, ее следует выбирать вместо значения по умолчанию, которое равно self._field[name] = value. Базовый класс теперь выглядит так (аналогично __getattr__):

class A(object):
    def __init__(self, fields=dict()):
        self._fields = fields

    def __getattr__(self, name):
        if hasattr(self, name):
            return object.__getattribute__(self, name)
        elif name in self._fields:
            return self._fields.get(name)
        else:
            raise AttributeError

    def __setattr__(self, name, value):
        if hasattr(self, name):
            object.__setattr__(self, name, value)
        elif name in self._fields:
            self._fields[name] = value
        else:
            raise AttributeError

Теперь, если я снова запущу тот же тест кода:

test = A_imp()
print test.val_1
print test.val_2
print test.val_3

Он мгновенно застревает в бесконечном цикле, он начинается с __setattr__, но сразу же переходит в __getattr__ и продолжает цикл.

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


person Yonathan    schedule 30.08.2017    source источник
comment
Это hasattr не нужно.   -  person o11c    schedule 30.08.2017


Ответы (1)


Невозможно проверить, есть ли у объекта атрибут, кроме как попытаться получить его. Таким образом,

hasattr(self, 'val_1')

на самом деле пытается получить доступ к self.val_1. Если self.val_1 не найден обычными средствами, он возвращается к __getattr__, который снова вызывает hasattr в бесконечной рекурсии.

hasattr на самом деле перехватывает любое исключение, включая RuntimeError: maximum recursion depth exceeded, и возвращает False, поэтому то, как именно это проявляется, зависит от того, сколько именно вложенных вызовов __getattr__. Некоторые вызовы __getattr__ попадают в случай object.__getattribute__ и вызывают AttributeError; некоторые попали в дело elif name in self._fields: return self._fields.get(name). Если self._fields существует, это возвращает значение, но с вашим __setattr__ иногда self._fields не существует!


Когда ваш __setattr__ пытается выполнить задание self._fields в __init__, он вызывает hasattr(self, '_fields'), который вызывает __getattr__. Теперь некоторые вызовы __getattr__ делают два рекурсивных вызова __getattr__, один в hasattr и один в elif name in self._fields. Поскольку hasattr перехватывает исключения, это вызывает экспоненциальные рекурсивные вызовы по глубине рекурсии вместо того, чтобы быстро либо работать, либо вызывать исключение.


Не используйте hasattr в __getattr__ или __getattribute__. В общем, будьте очень осторожны со всеми попытками доступа к атрибутам внутри методов, которые обрабатывают доступ к атрибутам.

person user2357112 supports Monica    schedule 30.08.2017
comment
Итак, как я могу это сделать? Я хочу иметь приоритет над самореализованными дескрипторами вместо того, чтобы всегда обслуживать записи self._field. - person Yonathan; 30.08.2017
comment
@Yonathan: __getattr__ вызывается только в том случае, если обычный поиск (через __getattribute__) не находит значения, поэтому вам не нужно проверять снова. Что касается того, как быть очень осторожным с доступом к атрибутам в целом, это обычно включает в себя делегирование super().__getattribute__ или super().__setattr__ для любого извлечения или назначения атрибута, которое необходимо обойти вашу специальную логику. - person user2357112 supports Monica; 30.08.2017
comment
Но применение этого типа if name in self._fields, а также super().__(set|set)attr__(self, name[, value]) в функциях set и get attr для базового класса вызывает те же проблемы с рекурсией. Я не понимаю, как я мог бы сначала решить стандартное поведение, а в противном случае проверить проблему с self._fields. Сейчас предпочтение отдается функциям __(get|set)attr__ вместо реализованных дескрипторов. Извините, я просто не понимаю, как я мог решить эту проблему. - person Yonathan; 31.08.2017
comment
@Yonathan: self._fields - это небезопасный доступ в вашей текущей реализации. Вы могли бы иметь __getattr__ особый случай name=='_fields' и немедленно вызвать AttributeError. - person user2357112 supports Monica; 31.08.2017
comment
Тем не менее, все еще вызывает те же проблемы. Мой другой вариант заключался в создании пользовательских дескрипторов и создании их как объектов, как бы обертывающих их, но я думаю, что это довольно запутанно. Простой базовый класс, который содержит поля с информацией, и возможность переопределять функции установки и получения - это то, что мне нужно. - person Yonathan; 31.08.2017