gpt4 book ai didi

python - 设计 __eq__ 来比较 self 和 other 的 __dict__ 免受 RecursionError 的影响

转载 作者:太空狗 更新时间:2023-10-30 01:47:32 24 4
gpt4 key购买 nike

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

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

一些子类的字段包含对绑定(bind)到自身的方法的引用 - 这会导致 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)

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

第二个问题是 - 我应该在 __dict__ 上使用什么样的过滤器来保护 __eq__ 免受这个问题的影响?我的意思是 - 在self.method 上面的 PoC 简单地保存在一个列表中,但有时它可能在另一个结构中。过滤必须包括所有可能包含自引用的容器。

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


# obj is a child instance of the A class

obj.append(obj.child_method)

for method in obj.methods:
method()

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

最佳答案

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

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

  1. 绑定(bind)到每个方法的self是否相等?
  2. 如果是,实现该方法的函数是否相同?

第 1 步导致无限递归;在比较 __dict__ 时,它最终会比较绑定(bind)的方法,为此,它必须再次将对象相互比较,现在你又回到了起点,它永远持续下去。

我能随手想出的唯一“解决方案”是:

  1. 类似于 reprlib.recursive_repr装饰器(这将是非常 hacky,因为你会根据是否重新输入 __eq__ 来启发式地确定你是否正在比较与绑定(bind)方法相关的原因),或者
  2. 您存储的任何绑定(bind)方法的包装器,用身份测试替换相应 self 的相等性测试。

绑定(bind)方法的包装器至少并不糟糕。您基本上只需制作一个简单的表单包装器:

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)

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

self.methods = [IdentityComparableMethod(self.method)]

您可能想让 methods 本身通过额外的魔法来强制执行此操作(因此它只存储函数或 IdentityComparableMethod),但这是基本思想。

其他答案解决了更有针对性的过滤问题,这只是一种使过滤变得不必要的方法。

性能说明:我没有大量优化性能; __getattr__ 是反射(reflect)底层方法所有属性的最简单方式。如果你想让比较进行得更快,你可以在初始化时取出__self__,直接缓存在self上,避免调用__getattr__,改变__slots____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

这在比较速度上有很大的不同;在本地 %timeit 测试中,first == second 比较从 2.77 μs 下降到 1.05 μs。如果你愿意,你也可以缓存 __func__,但由于它是回退比较,所以根本不太可能被检查(而且你会减慢构建一个你不太可能进行优化的提示使用)。

或者,您可以手动为 __self____func__ 定义 @property,而不是缓存,这比原始属性慢(比较在 1.41 μs 内运行),但根本不会产生构建时间成本(因此,如果从未运行过比较,则无需支付查找成本)。

关于python - 设计 __eq__ 来比较 self 和 other 的 __dict__ 免受 RecursionError 的影响,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56277404/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com