Из за чего чтение переменной во вложенной функции до присваивания ей в той же функции вызывает UnboundLocal...

Из-за чего чтение переменной во вложенной функции до присваивания ей в той же функции вызывает UnboundLocalError, даже если снаружи такая переменная уже существует?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Python определяет область видимости переменной во время компиляции функции: любое присваивание имени внутри функции делает это имя локальным для всей функции, если не указаны nonlocal или global. Поэтому чтение имени до присваивания обращается к ещё неинициализированной локальной переменной и вызывает UnboundLocalError, а не использует внешнее значение.

Исторический контекст

Лексические области видимости позволяют вложенной функции обращаться к переменным внешней функции после завершения её вызова. Для предсказуемого поведения Python заранее анализирует присваивания и разделяет локальные, нелокальные и глобальные имена.

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

Постановка проблемы

Разработчик видит внешнюю переменную и ожидает, что вложенная функция прочитает её значение. Однако добавление присваивания тому же имени превращает его в локальное имя функции.

Это может проявиться только на определённой ветке выполнения, поэтому ошибка нередко обнаруживается не при создании замыкания, а лишь при вызове. Неправильное исправление через global способно создать неявное изменение общего состояния и побочные эффекты между вызовами.

Подробное решение

При анализе функции Python классифицирует имя с присваиванием как локальное, если в функции нет объявления global или nonlocal. Это относится не только к обычному присваиванию, но и, например, к оператору +=, поскольку он включает операцию присваивания.

Если нужно изменить переменную внешней функции, применяется nonlocal. Если требуется работать с именем модуля, применяется global; использовать его для состояния обычно менее безопасно.

def make_counter(): value = 10 def next_value(): nonlocal value value += 1 return value return next_value counter = make_counter() print(counter()) # 11

Здесь value не становится локальной переменной next_value: nonlocal указывает искать её в ближайшей внешней функции. Замыкание хранит ссылку на ячейку этой переменной, поэтому последующие вызовы изменяют одно и то же состояние.

Если переменную нужно только читать, nonlocal не требуется. Но если внутри функции появится присваивание, без nonlocal или global имя будет считаться локальным.

Важно отличать изменение содержимого объекта от переназначения имени. Внешний изменяемый список можно изменить через append без nonlocal, но присваивание имени новому списку уже потребует nonlocal.

Ситуация из практики

В обработчике запросов разработчик добавляет локальный счётчик вызовов и одновременно хочет прочитать значение из конфигурации внешней функции. После добавления присваивания возникает UnboundLocalError на строке чтения, хотя конфигурация была создана до обработчика.

Вариант с global быстро устраняет ошибку, но связывает обработчики через общее состояние модуля и усложняет тестирование. Вариант с изменяемым объектом иногда работает, однако скрывает намерение и создаёт риск конкурентных изменений.

Предпочтительное решение — явно использовать nonlocal для состояния конкретного замыкания либо передать состояние в отдельный вызываемый объект с атрибутом. Для небольшого локального состояния замыкание компактнее; для сложного состояния вызываемый объект лучше структурирует данные и поведение.

Что кандидаты часто упускают

  1. Почему ошибка называется UnboundLocalError, а не NameError?

    UnboundLocalError — специализированный случай ошибки обращения к локальному имени до присваивания. Компилятор уже решил, что имя локальное, поэтому Python не продолжает поиск по внешним областям, как при обычном отсутствии имени.

  2. Достаточно ли добавить nonlocal, если переменная находится в глобальной области?

    Нет. nonlocal ищет имя только в объемлющих функциях и не может ссылаться на модульную область. Для глобальной переменной используется global, хотя для управляемого состояния обычно лучше явно передать объект или использовать объект с атрибутами.

  3. Почему вызов метода список.append не требует nonlocal, а присваивание список = новый_список требует?

    В первом случае имя не переназначается: изменяется объект, на который оно уже ссылается. Во втором случае создаётся новая привязка имени, поэтому Python считает имя локальным и требует nonlocal, если должна измениться переменная внешней функции.