Почему возникает ошибка бесконечной рекурсии?

Вопрос или проблема

У меня есть следующий код:

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, вызывая рекурсию. Это можно подтвердить, посмотрев на адреса этих функций в памяти.

python.exe

Поскольку print(r()) вызывает lambda s из предыдущей строки, которая все еще ссылается на lambda r, и затем попадает в бесконечный цикл. Изменение имени переменных поможет предотвратить эту проблему.

Другой пример может быть:

def aoeu():
    r=aoeu()
    print(r())
aoeu()

Проблема в том, что ссылки на лямбды перезаписываются и исчезают.

Вот что я имею в виду, строка за строкой:

  1. Сначала вы объявили r = lambda: 5. Это заставит r указывать на адрес памяти, например 0x10 (просто пример).

  2. Затем вы делаете s = r. Теперь s также указывает на 0x10.

  3. Теперь вы сказали, что r = lambda: s() * 6. Предположим, что эта функция была создана по адресу памяти 0x20 (таким образом, r указывает на 0x20). Вы можете подумать, что вызов s() здесь будет вызовом по адресу 0x10, о котором мы говорили выше, но это не совсем так. Интерпретатор проверит, где функция s начинается в своей таблице символов. Он начнет выполнять в самом последнем значении, присвоенном s. На данный момент выполнения значение s фактически будет 0x10.

  4. Здесь вы снова сказали, что s = r. Теперь вы обновляете значение s до адреса новой функции r(), которую вы создали выше. Следовательно, s = 0x20.

  5. В конце концов, вы снова переопределяете r как r = lambda: s() // 2. Предположим, что это было создано по адресу 0x30, так что r = 0x30.

Подводя итог

Адреса функций в таблице символов будут:

  • r = 0x30
  • s = 0x20

Выполнение

  1. Когда вы вызываете print(r()), интерпретатор проверит, где начинается функция r, и увидит, что в данном случае r начинает выполняться по адресу 0x30.

  2. Код в 0x30 это s() // 2. Обратите внимание, что здесь есть вызов s().

  3. Затем интерпретатор проверит, где начинается s, и увидит 0x20. А какой код у нас в 0x20? Это s() * 6!

Теперь вы увидите, почему вы застряли в бесконечной рекурсии. В 0x20 у вас есть вызов s(), верно? А s снова указывает на 0x20! Вот ваша бесконечная рекурсия.

Чтобы решить это, вам нужно создать разные переменные для каждой функции, как предлагали другие. Надеюсь, это поможет!

Хороший вопрос. Это связано с изменяемостью внутри замыканий в Python, которые «липкие». Кратко: замыкание внутри r изменяется на строке 5, заставляя s указывать на само себя.

Длинная версия:
Адрес s внутри замыкания, созданного на строке 4, изменяется присвоением s на строке 5: нелогично. Это означает, что s указывает на само себя к строке 7, что иллюстрируется в этой схеме указателей (вложенные коробки представляют замыкания):

схема указателей r и s

Схема демонстрирует, что адрес, на который указывает s, является тем же, который хранит s внутри своего собственного замыкания: s фактически указывает на само себя.

Вот полная карта состояния программы, с ярким использованием сокращений: форматы следующие —

name@number означает состояние переменной в данной строке;

address (из выполнения pdb в VSCode);

body (что находится в ячейке там);

[возможно] 'will loop', означает, что вызов функции по этому адресу приведет к расхождению.

карта состояния программы

Факт того, что стандартное присвоение переменных в Python изменяет внутренности замыканий, является распространенным источником трудноуловимых ошибок. Примеры вроде этого помогают выявлять их до того, как они могут возникнуть.

Редактировать: мой ответ касается только ‘почему’; предыдущие ответы дают хорошие решения альтернатив.

Ответ или решение

Ваша проблема с бесконечной рекурсией в приведённом коде объясняется взаимной ссылкой между лямбда-функциями r и s, что приводит к циклу вызовов. Давайте разберёмся в этом подробно.

Пояснение проблемы

  1. Сначала вы определяете r как лямбда-функцию, возвращающую 5:
    r = lambda: 5
  2. Затем вы присваиваете s значение r, так что обе переменные ссылаются на одну и ту же лямбда-функцию:
    s = r
  3. Далее вы переопределяете r, присваивая ему новую лямбда-функцию, которая вызывает s() и умножает результат на 6:
    r = lambda: s() * 6

    На этом этапе s всё ещё ссылается на старую версию r, которая возвращает 5. Однако также важно заметить, что всякий раз, когда вы вызываете s(), фактически вы обращаетесь к старой версии r.

  4. Затем снова переопределяете s:
    s = r

    Теперь s ссылается на новую версию r, которая зависит от s.

  5. Наконец, вы определяете 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 обрабатывает область видимости переменных, поможет вам избежать подобных проблем в будущем.

Оцените материал
Добавить комментарий

Капча загружается...