Программирование PythonPython CorePython-разработчик серверной части

Разбор ошибки: определите результат вызова show и объясните, какое правило областей видимости срабатывает. ...

Разбор ошибки: определите результат вызова show() и объясните, какое правило областей видимости срабатывает.

value = 10

def show():
    print(value)
    value = 20

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

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

Вызов show() завершится исключением UnboundLocalError, потому что value считается локальной переменной функции. Присваивание value = 20 заставляет Python трактовать все обращения к value внутри show() как обращения к локальному имени, включая обращение до присваивания.

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

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

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

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

В примере разработчик ожидает, что print(value) прочитает глобальное значение 10, а следующая строка изменит его. Однако наличие присваивания превращает value в локальное имя всей функции.

До выполнения value = 20 локальная переменная ещё не получила значение. Чтение такого имени вызывает UnboundLocalError, а не поиск значения в глобальной области. Неверное понимание этого правила приводит к ошибкам при добавлении счётчиков, изменении настроек и рефакторинге функций.

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

При компиляции функции Python видит присваивание value = 20 и классифицирует value как локальную переменную. Во время выполнения инструкция print(value) пытается прочитать локальную переменную до её инициализации, поэтому возникает UnboundLocalError.

Если требуется только прочитать глобальное имя, присваивание внутри функции нужно убрать:

value = 10 def show(): print(value) show() # 10

Если функция действительно должна изменить глобальную переменную, используется явное объявление global:

value = 10 def update(): global value value = 20 update() print(value) # 20

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

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

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

В обработчике запросов хранился глобальный лимит:

limit = 100 def next_limit(): limit += 10 return limit

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

Практическим решением будет объект с явно принадлежащим ему состоянием:

class Limiter: def __init__(self, limit=100): self.limit = limit def next_limit(self): self.limit += 10 return self.limit

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

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

  1. Дополнительный вопрос: Что выведет функция, если присваивание выполняется только в условной ветке?
value = 10 def show(flag): if flag: value = 20 return value

Ответ: при show(False) возникнет UnboundLocalError. Для компилятора достаточно самого факта присваивания в теле функции: value локальна независимо от того, выполнится ли ветка. Условие влияет на инициализацию во время выполнения, но не на классификацию имени.

  1. Дополнительный вопрос: Почему value += 1 вызывает ту же проблему, что и value = value + 1?

Ответ: составное присваивание включает чтение старого значения и последующую запись нового. Если имя локальное, его чтение происходит до локальной инициализации, поэтому возникает UnboundLocalError. Оператор += не превращает глобальное имя в изменяемое без объявления global.

  1. Дополнительный вопрос: Как изменить внешнюю переменную из вложенной функции, если она не глобальная?

Ответ: используется объявление nonlocal, которое связывает имя с ближайшей внешней функциональной областью:

def counter(): value = 0 def increment(): nonlocal value value += 1 return value return increment

Здесь value не является глобальной: вложенная функция изменяет переменную из области counter. nonlocal требует существующего имени во внешней функции; для глобальной области применяется global.