Подтвердить что ты не робот

Аргумент Распаковка отходов

Когда функция вызывается распаковкой аргументов, она, как представляется, увеличивает глубину рекурсии в два раза. Я хотел бы знать, почему это происходит.

Обычно:

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):

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
4b9b3361

Ответ 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;

Обратите внимание на пустое сообщение. Это ключевое различие.

Для вашей "регулярной" функции (без переменных аргументов) происходит то, что выбран оптимальный путь; функция Python, которая не нуждается в поддержке распаковки аргументов кортежа или ключевого слова, обрабатывается непосредственно в fast_function() function цикла оценки. Создается новый программный объект с байт-кодом Python для этой функции и запускается. Это одна проверка рекурсии.

Но для вызова функции с переменными аргументами (кортежем или словарем или обоими) вызов fast_function() не может быть использован. Вместо этого используется ext_do_call() (расширенный вызов), который обрабатывает распаковку аргументов, а затем использует PyObject_Call(), чтобы вызвать функцию. PyObject_Call() выполняет проверку предела рекурсии и "вызывает" объект функции. Объект функции вызывается с помощью функции function_call(), которая вызывает PyEval_EvalCodeEx(), который вызывает PyEval_EvalFrameEx(), что делает вторую проверку предела рекурсии.

TL; версия DR

Функции Python, вызывающие функции Python, оптимизируются и обходят функцию PyObject_Call() C-API, если не происходит распаковка аргументов. Выполнение календаря Python и PyObject_Call() делают реверсионные предельные тесты, поэтому в обход PyObject_Call() избегается приращение проверки ограничения рекурсии на вызов.

Дополнительные места с проверкой глубины рекурсии

Вы можете grep исходный код Python для Py_EnterRecursiveCall для других мест, где выполняются проверки глубины рекурсии; различные библиотеки, такие как 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