Работа с переменными в Python кажется очевидной до тех пор, пока код не начинает вести себя неожиданно. Ошибки с UnboundLocalError, странное поведение замыканий или некорректная работа global и nonlocal - всё это следствие непонимания области видимости.
В Python действует чёткое правило разрешения имён - LEGB. Разберёмся, как оно работает и какие ловушки скрываются под капотом.
Правило LEGB
Когда Python встречает имя переменной, он ищет его в следующем порядке:
L - Local - локальная область функции
E - Enclosing - внешняя функция (замыкание)
G - Global - глобальная область модуля
B - Built-in - встроенное пространство имён
Поиск происходит строго сверху вниз.
Local - локальная область
Любая переменная, присвоенная внутри функции, становится локальной.
x = 10 def func(): x = 20 print(x) func() # 20 print(x) # 10
Внутри func создаётся новая локальная переменная x, которая не влияет на глобальную.
Подводный камень: UnboundLocalError
x = 10 def func(): print(x) x = 20 func()
Ошибка:
UnboundLocalError: local variable 'x' referenced before assignment
Почему?
Потому что наличие x = 20 заставляет Python считать x локальной переменной во всей функции, даже до её присвоения.
Global - работа с глобальными переменными
Если нужно изменить глобальную переменную внутри функции, используется global.
x = 10 def func(): global x x = 20 func() print(x) # 20
Когда использовать global - плохая идея
Глобальные переменные усложняют тестирование
Повышают связность кода
Создают скрытые зависимости
В большинстве случаев лучше передавать значения аргументами.
Enclosing - внешняя область (замыкания)
Рассмотрим вложенную функцию:
def outer(): x = 10 def inner(): print(x) inner() outer()
nonlocal - изменение переменной внешней функции
Если нужно изменить переменную enclosing-области:
def outer(): x = 10 def inner(): nonlocal x x = 20 inner() print(x) outer() # 20
Без nonlocal возникла бы локальная переменная.
Замыкания (Closures)
Замыкание - это функция, которая:
запоминает окружение
хранит ссылки на переменные enclosing-области
Пример:
def multiplier(n): def multiply(x): return x * n return multiply times_two = multiplier(2) print(times_two(5)) # 10
Переменная n остаётся доступной даже после завершения multiplier.
Это возможно потому, что функция multiply хранит ссылку на окружение.
Подводный камень: замыкания в циклах
Классическая ошибка:
funcs = [] for i in range(3): def f(): return i funcs.append(f) print([f() for f in funcs]) # [2, 2, 2]
Замыкание хранит ссылку на переменную, а не её значение.
К моменту вызова i равно 2.
Исправление через аргумент по умолчанию
funcs = [] for i in range(3): def f(i=i): return i funcs.append(f) print([f() for f in funcs]) # [0, 1, 2]
Значение фиксируется в момент создания функции.
Built-in область
Если имя не найдено выше, Python ищет его во встроенном пространстве:
print(len([1, 2, 3]))
len находится во встроенной области.
Можно даже "перекрыть" встроенную функцию:
len = 10 print(len) # 10
Но это крайне плохая практика.
Как Python определяет область видимости
Важно понимать:
Python определяет области видимости на этапе компиляции функции, а не во время выполнения.
Именно поэтому возникает UnboundLocalError - интерпретатор заранее знает, что переменная локальная.
Влияние области видимости на архитектуру
Неправильное понимание LEGB приводит к:
скрытым зависимостям
трудно воспроизводимым багам
сложной логике замыканий
неожиданному поведению функций
Хорошие практики:
избегать
globalминимизировать
nonlocalявно передавать данные в функции
не перекрывать встроенные имена
понимать разницу между значением и ссылкой
Итог
LEGB - это фундаментальный механизм Python, который определяет, как разрешаются имена переменных.
Понимание:
Local
Enclosing
Global
Built-in
Позволяет: избегать логических ошибок, писать предсказуемый код, правильно использовать замыкания, проектировать более чистую архитектуру.
Если тема функций в Python вам интересна, больше практических примеров и разборов вы найдете на моем бесплатном курсе на Stepik.
