如何获取 Python 解释器堆栈的当前深度?

How do I get the current depth of the Python interpreter stack?

来自documentation

sys.getrecursionlimit()

Return the current value of the recursion limit, the maximum depth of the Python interpreter stack. This limit prevents infinite recursion from causing an overflow of the C stack and crashing Python. It can be set by setrecursionlimit().

我目前在酸洗对象时达到了递归限制。我正在 pickle 的对象只有几层嵌套,所以我对发生的事情有点困惑。

我已经能够通过以下 hack 来绕过这个问题:

try:
    return pickle.dumps(x)
except:
    try:
        recursionlimit = getrecursionlimit()
        setrecursionlimit(2*recursionlimit)
        dumped = pickle.dumps(x)
        setrecursionlimit(recursionlimit)
        return dumped
    except:
        raise

在不同的上下文中测试上述代码片段有时会在第一个 try 上取得成功,有时会在第二个 try 上取得成功。到目前为止,我还不能使它 raise 成为例外。

为了进一步调试我的问题,有一种方法可以获得堆栈的当前深度会很有帮助。这将允许我验证进入堆栈深度是否决定上面的代码片段是否会在第一个 try 或第二个上成功。

标准库有没有提供获取栈深度的函数,如果没有,如何获取?

def get_stack_depth():
    # what goes here?

您可以从 inspect.stack() 看到整个调用堆栈,因此当前获取的深度为 len(inspect.stack(0))

另一方面,我猜你在引发 "maximum recursion depth exceeded" 异常时打印出了完整的堆栈。该堆栈跟踪应该准确地告诉您出了什么问题。

如果速度有问题,绕过检查模块会更快。

testing depth: 50 (CPython 3.7.3)
stacksize4b()         | depth: 50   |    2.0 µs
stacksize4b(200)      | depth: 50   |    2.2 µs
stacksize3a()         | depth: 50   |    2.4 µs
stacksize2a()         | depth: 50   |    2.9 µs
stackdepth2()         | depth: 50   |    3.0 µs
stackdepth1()         | depth: 50   |    3.0 µs
stackdepth3()         | depth: 50   |    3.4 µs
stacksize1()          | depth: 50   |    7.4 µs  # deprecated
len(inspect.stack())  | depth: 50   |    1.9 ms

我将函数的名称缩短为 stacksize(),为了便于区分,我将 @lunixbochs 的函数称为 stackdepth()


基本算法:

对于小堆栈大小,这可能是代码简洁性、可读性和速度之间的最佳折衷。对于 ~10 帧以下,由于开销较低,只有 stackdepth1() 稍微快一些。

from itertools import count

def stack_size2a(size=2):
    """Get stack size for caller's frame.
    """
    frame = sys._getframe(size)

    for size in count(size):
        frame = frame.f_back
        if not frame:
            return size

为了为更大的堆栈大小实现更好的时序,一些更精细的算法是可能的。 stacksize3a() 将链式属性查找与 stackdepth1() 的近距离完成相结合,以获得更有利的计时斜率,在我的基准测试中开始获得大约 > 70 帧的回报。

from itertools import count

def stack_size3a(size=2):
    """Get stack size for caller's frame.
    """
    frame = sys._getframe(size)
    try:
        for size in count(size, 8):
            frame = frame.f_back.f_back.f_back.f_back.\
                f_back.f_back.f_back.f_back
    except AttributeError:
        while frame:
            frame = frame.f_back
            size += 1
        return size - 1

高级算法:

正如@lunixbochs 在回答中提出的那样,sys._getframe() 基本上是 C 代码中的 stackdepth1()。虽然更简单的算法总是从堆栈顶部的现有帧开始它们的深度搜索 in Python,向下检查堆栈以查找更多现有帧,stacksize4b() 允许通过其 stack_hint 参数从任何级别开始搜索,并且可以根据需要向下或向上搜索堆栈。

在幕后,调用 sys._getframe() 始终意味着将堆栈从顶部框架向下移动到指定深度。由于 Python 和 C 之间的性能差异如此巨大,如果需要,在应用基本的近距离框架之前,多次调用 sys._getframe() 以找到更接近最深框架的框架仍然可以带来回报在 Python 中逐帧搜索 frame.f_back

from itertools import count

def stack_size4b(size_hint=8):
    """Get stack size for caller's frame.
    """
    get_frame = sys._getframe
    frame = None
    try:
        while True:
            frame = get_frame(size_hint)
            size_hint *= 2
    except ValueError:
        if frame:
            size_hint //= 2
        else:
            while not frame:
                size_hint = max(2, size_hint // 2)
                try:
                    frame = get_frame(size_hint)
                except ValueError:
                    continue

    for size in count(size_hint):
        frame = frame.f_back
        if not frame:
            return size

stacksize4b() 的使用理念是将大小提示置于预期堆栈深度的下限以实现快速启动,同时仍然能够应对每一个剧烈和短暂的变化在堆栈深度。

基准显示 stacksize4b(),默认 size_hint=8 并调整 size_hint=200。对于基准测试,3-3000 范围内的所有堆栈深度都经过测试,以显示 stacksize4b().

时序中的特征锯齿图案