引数の展開はスタックフレームを無駄にする 質問する

引数の展開はスタックフレームを無駄にする 質問する

関数が引数をアンパックして呼び出されると、再帰の深さが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_FUNCTIONvs 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に配置されます。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

おすすめ記事