设计__eq__,比较__dict__自身和其他安全性与RecursionError

Mir*_*rek 4 python recursion python-3.x

我偶然发现了真正奇怪的python 3问题,原因我不明白。

我想通过检查所有对象的属性是否相等来比较我的对象。

一些子类将具有包含对绑定到self的方法的引用的字段,这会导致 RecursionError

这是PoC:

class A:

    def __init__(self, field):
        self.methods = [self.method]
        self.field = field

    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return self.__dict__ == other.__dict__

    def method(self):
        pass


first = A(field='foo')
second = A(field='bar')

print(first == second)
Run Code Online (Sandbox Code Playgroud)

在python 3中运行上面的代码会引发问题RecursionError,我不确定为什么。似乎A.__eq__用来比较中保留的功能self.methods。所以我的第一个问题是-为什么?为什么__eq__调用该对象的对象以比较该对象的绑定函数?

第二个问题是- __dict__应该使用哪种过滤器来保护__eq__免受此问题的影响?我的意思是-在上方的PoC中,它self.method只是保存在一个列表中,但有时它可能处于另一个结构中。过滤必须包括所有可能包含自引用的容器。

需要澄清的一点是:我确实需要将该self.method函数保留在一个self.methods字段中。这里的用例类似于unittest.TestCase._cleanups-测试完成后将调用的方法堆栈。该框架必须能够运行以下代码:


# obj is a child instance of the A class

obj.append(obj.child_method)

for method in obj.methods:
    method()
Run Code Online (Sandbox Code Playgroud)

另一个说明:我唯一可以更改的代码是__eq__实现。

Sha*_*ger 5

“为什么__eq__调用该对象以比较该对象的绑定函数?”:

因为绑定方法通过以下算法进行比较:

  1. self每个方法的约束是否相等?
  2. 如果是这样,实现该方法的功能是否相同?

步骤1导致无限递归;在比较时__dict__,最终要比较绑定的方法,并且这样做,必须再次将对象进行比较,现在您就回到了开始的地方,并且一直持续下去。

我可以临时提出的唯一“解决方案”是:

  1. 诸如reprlib.recursive_repr装饰器之类的东西(这将是非常棘手的,因为您将试探性地确定是否要根据是否__eq__重新输入来比较与绑定方法相关的原因),或者
  2. 您存储的所有绑定方法的包装器,self用身份测试代替了对的相等测试。

绑定方法的包装至少并不糟糕。基本上,您只需要对表单进行简单的包装即可:

class IdentityComparableMethod:
    __slots__ = '_method',
    def __new__(cls, method):
        # Using __new__ prevents reinitialization, part of immutability contract
        # that justifies defining __hash__
        self = super().__new__(cls)
        self._method = method
        return self

    def __getattr__(self, name):
        '''Attribute access should match bound method's'''
        return getattr(self._method, name)

    def __eq__(self, other):
        '''Comparable to other instances, and normal methods'''
        if not isinstance(other, (IdentityComparableMethod, types.MethodType)):
            return NotImplemented
        return (self.__self__ is other.__self__ and
                self.__func__ is other.__func__)

    def __hash__(self):
        '''Hash identically to the method'''
        return hash(self._method)

    def __call__(self, *args, **kwargs):
        '''Delegate to method'''
        return self._method(*args, **kwargs)

    def __repr__(self):
        return '{0.__class__.__name__}({0._method!r})'.format(self)
Run Code Online (Sandbox Code Playgroud)

然后在存储绑定方法时,将它们包装在该类中,例如:

self.methods = [IdentityComparableMethod(self.method)]
Run Code Online (Sandbox Code Playgroud)

您可能希望methods自己通过其他魔术来强制执行此操作(因此它仅存储函数或IdentityComparableMethods),但这是基本思想。

其他答案则针对更有针对性的过滤,这只是使过滤不再必要的一种方法。

效果说明:我并未对性能进行过多的优化;__getattr__是反映基础方法的所有属性的最简单方法。如果您希望比较更快,可以__self__在初始化期间提取并self直接缓存在上面以避免__getattr__调用,将__slots__and __new__声明更改为:

    __slots__ = '_method', '__self__'
    def __new__(cls, method):
        # Using __new__ prevents reinitialization, part of immutability contract
        # that justifies defining __hash__
        self = super().__new__(cls)
        self._method = method
        self.__self__ = method.__self__
        return self
Run Code Online (Sandbox Code Playgroud)

这使得比较速度有了很大的不同。在本地%timeit测试中,first == second比较结果从2.77微秒下降到1.05微秒。__func__如果愿意,您也可以缓存,但是由于这是后备比较,因此根本就不会检查它(并且您会为不太可能使用的优化构建进度很慢)。

另外,除了缓存之外,您还可以手动@property__self__和定义s __func__,它们比原始属性要慢(比较的运行时间为1.41?s),但完全不会花费任何构建时间(因此,如果没有进行过比较,则不会) t支付查询费用)。