【问题标题】:Combining __setattr__ and __getattr__ causes infinite loop结合 __setattr__ 和 __getattr__ 导致无限循环
【发布时间】:2017-08-30 16:12:26
【问题描述】:

我正在尝试构建一个基类用于所有其他对象的系统。每个基对象内部都有一个_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 的问题,但无法弄清楚,这就是为什么我构建这个测试用例来清楚地弄清楚它的原因。希望有人能够为我澄清这一点并帮助我解决问题。

【问题讨论】:

  • hasattr 是不必要的。

标签: python class infinite-loop getattr setattr


【解决方案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__ 尝试处理__init__ 中的self._fields 分配时,它会调用hasattr(self, '_fields'),后者又会调用__getattr__。现在,一些__getattr__ 调用会进行两次递归__getattr__ 调用,一个在hasattr 中,一个在elif name in self._fields 中。由于hasattr 正在捕获异常,这会导致递归调用在递归深度上呈指数级增长,而不是很快看起来有效或引发异常。


不要在__getattr____getattribute__ 中使用hasattr。一般来说,在处理属性访问的方法中访问属性的所有尝试都要非常小心。

【讨论】:

  • 那么我该怎么做呢?我希望优先于自我实现的描述符,而不是始终为 self._field 条目提供服务。
  • @Yonathan: __getattr__ 仅在“正常”查找(通过__getattribute__)未找到值时调用,因此您实际上不必再次检查。至于如何在一般情况下“非常小心”访问属性,这通常涉及委托给super().__getattribute__super().__setattr__ 进行任何需要绕过您的特殊逻辑的属性检索或分配。
  • 但是像 if name in self._fields 这样应用它,否则它作为 super().__(set|set)attr__(self, name[, value]) 在基类的 set 和 get attr 函数中会导致相同的递归问题。我看不出如何解决“首先解决标准行为,否则检查 self._fields”问题。现在优先考虑__(get|set)attr__ 函数而不是实现的描述符。抱歉,我只是想念如何解决这个问题。
  • @Yonathan: self._fields 在您当前的实现中是不安全的访问。你可以有 __getattr__ 特殊情况 name=='_fields' 并立即引发 AttributeError。
  • 仍然会导致同样的问题。我的另一个选择是构建自定义描述符并将它们创建为对象,以将其包装起来,但我认为这非常复杂。一个简单的基类 thst 包含带有信息的字段,并且能够覆盖 setter 和 getter 函数是我所追求的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-17
  • 1970-01-01
  • 2018-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-27
相关资源
最近更新 更多