我正在重构一个python信号处理框架,因为我们遇到了一个最大的重复深度错误。
对于该错误最可能的解释是,有时单个类的大量实例是从类的init方法递归创建的。实际上,模拟这种情况的实验会导致一个异常的runtimeerror:"超过了最大递归深度"。
当我将创建链中的下一个元素移动到容器管理这些对象时,问题似乎消失了,尽管在我幼稚的理解中构建了相同深度的调用堆栈。我通过以前创建的所有对象调用create。(参见示例代码)。
我倾向于得出这样的结论:递归深度限制是以某种方式为每个对象设置的(无论是类还是实例)。当通过init递归创建时,可能会将所有内容都放在类的堆栈上,在这里,就像我们通过同一类的一行实例递归创建它们一样,所有对象的递归深度都非常有限。
我正在为这个假设寻求一些证实,但发现很难得到证实或反驳。
import sys
class Chunk(object):
pre=None
post=None
def __init__(self, container,pre):
print 'Chunk Created'
self.pre=pre
self.container=container
def create(self,x):
if self.post == None:
self.post=Chunk(self.container,self)
newChunk =self.post
else:
newChunk =self.post.create(x+1)
return newChunk
def droprefs(self):
print 'refcount droprefs start: ', sys.getrefcount(self)
if not self.pre ==None:
self.pre.droprefs()
self.pre=None
self.post=None
self.container=None
print 'refcount droprefs end: ', sys.getrefcount(self)
def claimContainer(self):
self.container.setmasterchunk(self)
self.pre.droprefs()
self.pre=None
class Container(object):
masterchunk=None
def __init__(self):
self.masterchunk=Chunk(self, None)
def setmasterchunk(self, chunk):
print 'refcount 5: ', sys.getrefcount(self.masterchunk)
self.masterchunk=chunk
print 'refcount 6: ', sys.getrefcount(self.masterchunk)
def testrecursion(self,n):
for i in range(n):
print 'refcount 6: ', sys.getrefcount(self.masterchunk)
newChunk=self.masterchunk.create(1)
return newChunk
def testhistoryremoval(self,chunk):
chunk.claimContainer()
if __name__ == '__main__':
C=Container()
middle=C.testrecursion(300)
last=C.testrecursion(600)
last=C.testhistoryremoval(middle)
if middle== C.masterchunk:
print 'SUCCESS'
添加的注释:
显示最大递归深度的代码:
import sys
from time import sleep
class Chunk(object):
pre=None
post=None
def __init__(self, container,pre,n):
print 'Chunk Created'
self.pre=pre
self.container=container
if n>0:
self.post=Chunk(self.container,self,n-1)
def droprefs(self):
print 'refcount droprefs start: ', sys.getrefcount(self)
if not self.pre ==None:
self.pre.droprefs()
self.pre=None
self.post=None
self.container=None
print 'refcount droprefs end: ', sys.getrefcount(self)
def claimContainer(self):
self.container.setmasterchunk(self)
self.pre.droprefs()
self.pre=None
class Container(object):
masterchunk=None
def __init__(self):
pass
def setmasterchunk(self, chunk):
print 'refcount 5: ', sys.getrefcount(self.masterchunk)
self.masterchunk=chunk
print 'refcount 6: ', sys.getrefcount(self.masterchunk)
def testrecursion(self,n):
self.masterchunk=Chunk(self,None,n)
if __name__ == '__main__':
print('Try 10')
C0=Container()
C0.testrecursion(10)
sleep(2)
print('Try 1000')
C1=Container()
C1.testrecursion(1000)
号
"当我将创建链中的下一个元素移动到管理这些对象的容器时,问题似乎就消失了"——很可能是因为您不再递归,或者递归的不够深。
您可以更改递归深度。有一种算法可以根据计算机的性能为您计算它。
相关:stackoverflow.com/a/26873813/2289509
信号处理不是计算密集型的吗?如果是这样,递归似乎不合适。
@Elazar信号处理是计算密集型的,但我所针对的代码是将输入信息降落到正确位置的机制,它先于实际的信号处理。当它完成并发布后,如果您感兴趣,我可以在这里放置一个更新。
您可以将方法重写为迭代的,而不是递归的。这避免了递归深度错误的可能性,不管数据结构有多深。
create方法会变成这样:
def create(self, x): # the `x` arg is not actually used for anything?
chunk = self
while chunk.post is not None:
chunk = chunk.post
chunk.post = Chunk(self.container, chunk)
return self.post # this matches the old code but you may actually want `return self.post`
你可以用droprefs做类似的事情。您当前的代码似乎是从列表的末尾向前迭代的,这可能是您想要的,也可能不是您想要的。下面是对迭代行为的直接转换:
def droprefs(self):
chunk = self
while chunk:
next = chunk.pre # this iterates backwards, to go forwards use `next = chunk.post`
chunk.pre = None
chunk.post = None
chunk.container = None
chunk = next
我还注意到,除非您希望外部代码包含对早期Chunk的引用,否则您可能根本不需要使用droprefs。在claimContainer执行self.pre = None之后,垃圾收集器应该能够清除所有早期的Chunk实例,因为它们没有更多的外部引用。消除它们之间的引用可能会使其更快地工作(因为pre和post属性形成了引用循环),但这并不是严格必要的。
这是我正在计划的解决方案,但它需要大量的工作,可能会破坏我的代码。在开始这次冒险之前,我想确保我对调用堆栈有足够的了解。原始代码已经进行了大量的引用移除,以允许垃圾收集器完成其工作。
在我使用的实现中,设置
sys.setrecursionlimit(907)
演示这是您达到的递归深度。
对于您的问题,答案是否定的。在cpython的源代码中,您可以看到递归深度是每个线程,而不是每个对象。
PySt.H:
typedef struct _ts {
/* See Python/ceval.c for comments explaining most fields */
//...
int recursion_depth;
//...
}
C:
/* the macro Py_EnterRecursiveCall() only calls _Py_CheckRecursiveCall()
if the recursion_depth reaches _Py_CheckRecursionLimit.
If USE_STACKCHECK, the macro decrements _Py_CheckRecursionLimit
to guarantee that _Py_CheckRecursiveCall() is regularly called.
Without USE_STACKCHECK, there is no need for this. */
int
_Py_CheckRecursiveCall(const char *where)
{
PyThreadState *tstate = PyThreadState_GET();
//...
if (tstate->recursion_depth > recursion_limit) {
--tstate->recursion_depth;
tstate->overflowed = 1;
PyErr_Format(PyExc_RecursionError,
"maximum recursion depth exceeded%s",
where);
return -1;
}
//...
}
tstate是线程状态的简写。
这是如何用我现在添加到原始问题的第二个示例解释行为上的差异的呢?线程是在一个场景中自动创建的,而不是在另一个场景中自动创建的?
@我在这两个例子中都做了一些练习,不同之处似乎是程度的问题。如果init场景的创建已经停止在300的"原始递归深度",那么第一个场景将停止在900到1000之间的"原始递归深度"。所以假设python在实现构造函数时添加了一些函数调用(我认为这是有道理的)。因此,结论是:当链变长时,通过对象链递归到达链中的最后一个对象不是一个好的解决方案。
@罗纳德范尔伯格,你回答错误。但是,我不明白为什么你认为递归300和600会达到最大深度1000。
因为第二组600遍历在第一次调用中创建的对象,所以递归深度为900。这仍然有效。如果我把它调高到1000,它就失败了。我认为我之前的评论证实了你和普拉恩的回答。这就是为什么我在这里放了一个评论,并引用了prune。如果有更好的方法来处理这件事,请告诉我。
不,这是全局递归深度。您描述的情况意味着当您超过堆栈限制时,您捕获异常并继续处理下一个对象。这个过程删除相关的堆栈项——将递归分解到其起点。
从下一个对象开始。如果这个堆栈没有超过堆栈深度,它将顺利完成——先前的堆栈条目不会影响新的堆栈。