関数が引数をアンパックして呼び出されると、再帰の深さが2倍になるようです。なぜこういうことが起こるのです。
通常は:
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 対 1395、1526 対 1395)。
逆アセンブリからわかるように、呼び出しのタイプ ( CALL_FUNCTION
vs CALL_FUNCTION_VAR
) 以外に、2 つの間にはほとんど違いはありません。
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
例外メッセージ実際にヒントを提供します。解凍しないオプションと比較してください。
>>> 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()
関数を設定すると、この例外は表示されません。奇数再帰制限:
>>> 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
それは、ceval.c
フレーム評価コード内部PyEval_EvalFrameEx()
:
/* push frame */
if (Py_EnterRecursiveCall(""))
return NULL;
そこに空のメッセージがあることに注意してください。これが重要な違いです。
'通常の'関数(可変引数なし)の場合、最適化されたパス選ばれる;パイソンタプルやキーワード引数の展開サポートを必要としない関数は、fast_function()
関数評価ループの終了。関数のPythonバイトコードオブジェクトを含む新しいフレームオブジェクトが作成され、実行されます。これは1つ再帰チェック。
しかし、可変引数(タプルまたは辞書、あるいはその両方)を持つ関数呼び出しでは、fast_function()
呼び出しは使用できません。代わりに、ext_do_call()
(延長通話)が使用され、引数の展開を処理し、次にPyObject_Call()
関数を呼び出す。PyObject_Call()
再帰制限チェックを行い、関数オブジェクトを「呼び出し」ます。関数オブジェクトは、function_call()
関数、これはPyEval_EvalCodeEx()
、これはPyEval_EvalFrameEx()
、これにより2番再帰制限チェック。
TL;DRバージョン
Python関数を呼び出すPython関数は最適化され、PyObject_Call()
C-API関数をバイパスします。ない限り引数の展開が行われます。Python フレーム実行とPyObject_Call()
再帰制限テストの両方が実行されるため、バイパスするPyObject_Call()
と呼び出しごとに再帰制限チェックが増加するのを回避できます。
追加の再帰深度チェックを行う場所が増える
Py_EnterRecursiveCall
再帰の深さのチェックが行われる他の場所については、Python ソース コードを grep で検索できます。json
やなどのさまざまなライブラリは、たとえば、ネストが深すぎる構造や再帰的な構造の解析を回避するためにこれを使用します。 その他のチェックは、と の実装、高度な比較 ( 、、など)、呼び出し可能オブジェクト フックの処理、呼び出しの処理pickle
に配置されます。list
tuple
__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