Вопрос или проблема
У меня есть следующий код:
def aoeu():
r = lambda: 5
s = r
r = lambda: s() * 6
s = r
r = lambda: s() // 2
print(r())
При выполнении возникает ошибка:
File "/Users/nyap/tasks/spreadsheet/spreadsheet/aoeu.py", line 10, in <lambda>
r = lambda: s() * 6
^^^
[Предыдущая строка повторялась еще 993 раза]
RecursionError: превышена максимальная глубина рекурсии
То же самое происходит при использовании s = copy.deepcopy(r)
и даже:
def s():
return r()
и:
def s(r):
return r()
Почему? И можно ли это исправить так, чтобы это не происходило, не вводя другую переменную, так как в самом коде есть цикл?
Это для python-3.12.6
Возможно, нам стоит сделать названия переменных (r, s) более понятными.
def aoeu():
# r = lambda: 5
# s = r
# r = lambda: s() * 6
# s = r
# r = lambda: s() // 2
s1 = lambda: 5
s2 = lambda: s1() * 6
r = lambda: s2() // 2
print(r())
Проблема, с которой вы сталкиваетесь, возникает из-за того, что лямбда-функции r
и s
вызывают друг друга рекурсивно в круговой манере, что вызывает ошибку рекурсии.
Чтобы избежать этой ошибки рекурсии, не вводя дополнительные переменные, вы можете использовать одну лямбда-функцию, которая захватывает текущее состояние и обновляет его с каждой присваиванием. Вот пример того, как вы можете переписать код, чтобы избежать ошибки рекурсии:
def aoeu():
state = {"value": 5}
r = lambda: state["value"]
s = lambda: state["value"] * 6
state["value"] = s()
s = lambda: state["value"] // 2
print(r())
aoeu()
Используя словарь (state
) для хранения текущего значения и обновляя его внутри лямбда-функций, вы можете избежать ошибки рекурсии, вызванной круговыми ссылками между r
и s
.
Когда r
вызывается в операторе print
, он вызовет s
. s
является тем же, что предыдущее определение r
, которое снова вызывает s
, вызывая рекурсию. Это можно подтвердить, посмотрев на адреса этих функций в памяти.
Поскольку print(r())
вызывает lambda s
из предыдущей строки, которая все еще ссылается на lambda r
, и затем попадает в бесконечный цикл. Изменение имени переменных поможет предотвратить эту проблему.
Другой пример может быть:
def aoeu():
r=aoeu()
print(r())
aoeu()
Проблема в том, что ссылки на лямбды перезаписываются и исчезают.
Вот что я имею в виду, строка за строкой:
-
Сначала вы объявили
r = lambda: 5
. Это заставитr
указывать на адрес памяти, например 0x10 (просто пример). -
Затем вы делаете
s = r
. Теперьs
также указывает на 0x10. -
Теперь вы сказали, что
r = lambda: s() * 6
. Предположим, что эта функция была создана по адресу памяти 0x20 (таким образом,r
указывает на 0x20). Вы можете подумать, что вызовs()
здесь будет вызовом по адресу 0x10, о котором мы говорили выше, но это не совсем так. Интерпретатор проверит, где функцияs
начинается в своей таблице символов. Он начнет выполнять в самом последнем значении, присвоенномs
. На данный момент выполнения значениеs
фактически будет 0x10. -
Здесь вы снова сказали, что
s = r
. Теперь вы обновляете значениеs
до адреса новой функцииr()
, которую вы создали выше. Следовательно,s = 0x20
. -
В конце концов, вы снова переопределяете
r
какr = lambda: s() // 2
. Предположим, что это было создано по адресу 0x30, так чтоr = 0x30
.
Подводя итог
Адреса функций в таблице символов будут:
r = 0x30
s = 0x20
Выполнение
-
Когда вы вызываете
print(r())
, интерпретатор проверит, где начинается функцияr
, и увидит, что в данном случаеr
начинает выполняться по адресу 0x30. -
Код в 0x30 это
s() // 2
. Обратите внимание, что здесь есть вызовs()
. -
Затем интерпретатор проверит, где начинается
s
, и увидит 0x20. А какой код у нас в 0x20? Этоs() * 6
!
Теперь вы увидите, почему вы застряли в бесконечной рекурсии. В 0x20 у вас есть вызов s()
, верно? А s
снова указывает на 0x20! Вот ваша бесконечная рекурсия.
Чтобы решить это, вам нужно создать разные переменные для каждой функции, как предлагали другие. Надеюсь, это поможет!
Хороший вопрос. Это связано с изменяемостью внутри замыканий в Python, которые «липкие». Кратко: замыкание внутри r
изменяется на строке 5, заставляя s
указывать на само себя.
Длинная версия:
Адрес s
внутри замыкания, созданного на строке 4, изменяется присвоением s
на строке 5: нелогично. Это означает, что s
указывает на само себя к строке 7
, что иллюстрируется в этой схеме указателей (вложенные коробки представляют замыкания):
Схема демонстрирует, что адрес, на который указывает s
, является тем же, который хранит s
внутри своего собственного замыкания: s
фактически указывает на само себя.
Вот полная карта состояния программы, с ярким использованием сокращений: форматы следующие —
name@number
означает состояние переменной в данной строке;
address
(из выполнения pdb
в VSCode);
body
(что находится в ячейке там);
[возможно] 'will loop'
, означает, что вызов функции по этому адресу приведет к расхождению.
Факт того, что стандартное присвоение переменных в Python изменяет внутренности замыканий, является распространенным источником трудноуловимых ошибок. Примеры вроде этого помогают выявлять их до того, как они могут возникнуть.
Редактировать: мой ответ касается только ‘почему’; предыдущие ответы дают хорошие решения альтернатив.
Ответ или решение
Ваша проблема с бесконечной рекурсией в приведённом коде объясняется взаимной ссылкой между лямбда-функциями r
и s
, что приводит к циклу вызовов. Давайте разберёмся в этом подробно.
Пояснение проблемы
- Сначала вы определяете
r
как лямбда-функцию, возвращающую5
:r = lambda: 5
- Затем вы присваиваете
s
значениеr
, так что обе переменные ссылаются на одну и ту же лямбда-функцию:s = r
- Далее вы переопределяете
r
, присваивая ему новую лямбда-функцию, которая вызываетs()
и умножает результат на6
:r = lambda: s() * 6
На этом этапе
s
всё ещё ссылается на старую версиюr
, которая возвращает5
. Однако также важно заметить, что всякий раз, когда вы вызываетеs()
, фактически вы обращаетесь к старой версииr
. - Затем снова переопределяете
s
:s = r
Теперь
s
ссылается на новую версиюr
, которая зависит отs
. - Наконец, вы определяете
r
снова:r = lambda: s() // 2
Теперь, если вызвать
r()
, выполнитсяs() // 2
, что вызовет новую версиюs
, которая опять вызываетs()
и так далее, образуя бесконечный цикл.
Итог
Когда вы вызываете print(r())
, Python проверяет, что делает r()
, и находит, что это вызов s() // 2
. s()
вызывает r()
, а r()
снова использует s()
, и цикл продолжается, пока не превышается максимальная глубина рекурсии.
Как избежать проблемы
Есть несколько способов избежать этой проблемы, не добавляя дополнительных переменных, однако основное решение заключается в использовании различных имен для функций. Это позволяет избежать путаницы и бесконечных ссылок. Например:
def aoeu():
base_value = 5
get_value = lambda: base_value
multiply = lambda: get_value() * 6
divide = lambda: multiply() // 2
print(divide())
aoeu()
В этом примере каждая лямбда-функция ссылается на свою собственную версию результата, а не переопределяет переменные, что позволяет избежать бесконечной рекурсии.
Заключение
Проблемы, связанные с циклической ссылкой между лямбда-функциями, часто являются источником труднодолжных в отладке ошибок. Понимание принципов работы замыканий и того, как Python обрабатывает область видимости переменных, поможет вам избежать подобных проблем в будущем.