参数解包浪费堆栈帧

时间:2014-05-27 00:21:47

标签: python recursion cpython python-internals

当通过解包参数调用函数时,它似乎会增加两次递归深度。我想知道为什么会发生这种情况。

通常:

depth = 0

def f():
    global depth
    depth += 1
    f()

try:
    f()
except RuntimeError:
    print(depth)

#>>> 999

通过解压缩电话:

depth = 0

def f():
    global depth
    depth += 1
    f(*())

try:
    f()
except RuntimeError:
    print(depth)

#>>> 500

理论上两者都应该达到1000左右:

import sys
sys.getrecursionlimit()
#>>> 1000

这发生在CPython 2.7和CPython 3.3上。

在PyPy 2.7和PyPy 3.3上存在差异,但它要小得多(1480 vs 1395和1526 vs 1395)。


从反汇编中可以看出,除了通话类型(CALL_FUNCTION vs CALL_FUNCTION_VAR)之外,两者之间几乎没有差异:

import dis
def f():
    f()

dis.dis(f)
#>>>  34           0 LOAD_GLOBAL              0 (f)
#>>>               3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
#>>>               6 POP_TOP
#>>>               7 LOAD_CONST               0 (None)
#>>>              10 RETURN_VALUE
def f():
    f(*())

dis.dis(f)
#>>>  47           0 LOAD_GLOBAL              0 (f)
#>>>               3 BUILD_TUPLE              0
#>>>               6 CALL_FUNCTION_VAR        0 (0 positional, 0 keyword pair)
#>>>               9 POP_TOP
#>>>              10 LOAD_CONST               0 (None)
#>>>              13 RETURN_VALUE

1 个答案:

答案 0 :(得分:18)

异常消息实际上为您提供了提示。比较非解包选项:

>>> import sys
>>> sys.setrecursionlimit(4)  # to get there faster
>>> def f(): f()
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in f
RuntimeError: maximum recursion depth exceeded

使用:

>>> def f(): f(*())
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in f
RuntimeError: maximum recursion depth exceeded while calling a Python object

请注意添加while calling a Python object。此例外特定PyObject_CallObject() function。设置奇数递归限制时,您不会看到此异常:

>>> sys.setrecursionlimit(5)
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in f
  File "<stdin>", line 1, in f
RuntimeError: maximum recursion depth exceeded

因为这是PyEval_EvalFrameEx()ceval.c frame evaluation code内提出的具体例外:

/* push frame */
if (Py_EnterRecursiveCall(""))
    return NULL;

请注意那里的空信息。这是一个至关重要的区别。

对于您的常规&#39;函数(没有变量参数),会发生的是选择优化路径;一个不需要元组或关键字参数解包支持的 Python 函数直接在评估循环的fast_function() function中处理。创建并运行具有该函数的Python字节码对象的新frameobject。这是一次递归检查。

但是对于带有可变参数的函数调用(元组或字典或两者),不能使用fast_function()调用。相反,使用ext_do_call() (extended call)来处理参数解包,然后使用PyObject_Call()来调用该函数。 PyObject_Call()执行递归限制检查,然后调用&#39;功能对象。函数对象是通过function_call() function调用的,PyEval_EvalCodeEx()调用PyEval_EvalFrameEx(),调用{{3}},这将进行第二次递归限制检查。

TL; DR版本

调用Python函数的Python函数已经过优化,并绕过PyObject_Call() C-API函数,除非发生参数解包。 Python帧执行和PyObject_Call()都进行递归限制测试,因此绕过PyObject_Call()可以避免每次调用递增递归限制检查。

更多地方有&#39;额外&#39;递归深度检查

对于进行递归深度检查的其他位置,您可以为Py_EnterRecursiveCall grep Python源代码;例如,jsonpickle等各种库使用它来避免解析嵌套过度或递归的结构。其他检查放在listtuple __repr__实施中,丰富的比较(__gt____lt____eq__等),处理__call__可调用对象挂钩和处理__str__调用。

因此,您可以更快地达到递归限制

>>> class C:
...     def __str__(self):
...         global depth
...         depth += 1
...         return self()
...     def __call__(self):
...         global depth
...         depth += 1
...         return str(self)
... 
>>> depth = 0
>>> sys.setrecursionlimit(10)
>>> C()()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in __call__
  File "<stdin>", line 5, in __str__
RuntimeError: maximum recursion depth exceeded while calling a Python object
>>> depth
2