Работа с переменными в Python кажется очевидной до тех пор, пока код не начинает вести себя неожиданно. Ошибки с UnboundLocalError, странное поведение замыканий или некорректная работа global и nonlocal - всё это следствие непонимания области видимости.

В Python действует чёткое правило разрешения имён - LEGB. Разберёмся, как оно работает и какие ловушки скрываются под капотом.

Правило LEGB

Когда Python встречает имя переменной, он ищет его в следующем порядке:

  1. L - Local - локальная область функции

  2. E - Enclosing - внешняя функция (замыкание)

  3. G - Global - глобальная область модуля

  4. 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.