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__实现。
“为什么__eq__调用该对象以比较该对象的绑定函数?”:
因为绑定方法通过以下算法进行比较:
self每个方法的约束是否相等?步骤1导致无限递归;在比较时__dict__,最终要比较绑定的方法,并且这样做,必须再次将对象进行比较,现在您就回到了开始的地方,并且一直持续下去。
我可以临时提出的唯一“解决方案”是:
reprlib.recursive_repr装饰器之类的东西(这将是非常棘手的,因为您将试探性地确定是否要根据是否__eq__重新输入来比较与绑定方法相关的原因),或者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支付查询费用)。