Области видимости в Python: LEGB, global и nonlocal — понятное руководство с...

Области видимости в Python: LEGB, global и nonlocal — понятное руководство с примерами

Области видимости в Python: LEGB, global и nonlocal — понятное руководство с примерами

Если ваш код «видит» не ту переменную или падает с UnboundLocalError, почти наверняка проблема в областях видимости. В этой статье мы разберём правило LEGB, а также ключевые слова global и nonlocal в Python — на практичных примерах, с советами и типичными ошибками новичков.

LEGB: как Python ищет переменные

LEGB — это порядок поиска имени переменной:

  • Local — локальная область (внутри текущей функции)
  • Enclosing — область внешних функций (для вложенных функций)
  • Global — глобальная область модуля
  • Builtins — встроенные имена Python (len, sum и др.)
  • Посмотрим на простой пример теневого перекрытия имён:

    x = 'global'
    
    def outer():
        x = 'enclosing'
        def inner():
            x = 'local'
            print(x)  # local
        inner()
        print(x)      # enclosing
    
    outer()
    print(x)          # global
    

    Python всегда берёт самое ближайшее определение по правилу LEGB.

    global в Python: когда нужен и когда нет

    Ключевое слово global говорит интерпретатору: «я собираюсь переназначать имя из глобальной области». Без этого Python посчитает имя локальным и может выдать UnboundLocalError.

    x = 10
    
    def bad_inc():
        print('Перед инкрементом:', x)  # Попытка прочитать локальное x до присваивания
        x = x + 1                       # UnboundLocalError: local variable 'x' referenced before assignment
        return x
    
    # bad_inc()
    
    def good_inc():
        global x
        x = x + 1
        return x
    
    print(good_inc())  # 11
    

    Важно: global нужен только при переприсваивании имени. Если вы мутируете глобальный изменяемый объект (список, словарь), global не требуется:

    nums = [1, 2]
    
    def add_three():
        nums.append(3)  # Мутация, работает без global
    
    def reset_wrong():
        nums = []       # Переприсваивание: создаётся локальное nums, глобальное не тронуты
    
    def reset_ok():
        global nums
        nums = []       # Явно переприсваиваем глобальное имя
    
    add_three()
    print(nums)  # [1, 2, 3]
    reset_wrong()
    print(nums)  # [1, 2, 3] — не сбросился!
    reset_ok()
    print(nums)  # []
    

    Стоит ли использовать global?

    В продакшене — редко. Глобальные изменяют состояние «изнутри», усложняя тестирование и сопровождение. Предпочтительнее передавать значения параметрами и возвращать результат:

    def inc(value):
        return value + 1
    
    x = 10
    x = inc(x)
    

    Если нужно хранить состояние — инкапсулируйте его в объект:

    class Counter:
        def __init__(self, start=0):
            self.value = start
        def inc(self, step=1):
            self.value += step
            return self.value
    
    c = Counter()
    print(c.inc(), c.inc())  # 1 2
    

    nonlocal: управление переменной внешней функции

    nonlocal нужен, когда у нас есть вложенные функции и мы хотим менять переменную не глобального, а внешнего локального уровня (enclosing). Без nonlocal возникнет UnboundLocalError, как и с global-сценарием.

    def outer():
        x = 0
        def inner_bad():
            x += 1              # UnboundLocalError: local variable 'x' referenced before assignment
            return x
        # return inner_bad
    
    # outer()()
    
    def outer_fix():
        x = 0
        def inner():
            nonlocal x          # объявляем, что x — из enclosing-области, не локальная
            x += 1
            return x
        return inner
    
    counter = outer_fix()
    print(counter(), counter(), counter())  # 1 2 3
    

    Классический паттерн с nonlocal — «функция‑счётчик» (замыкание):

    def make_counter(start=0, step=1):
        value = start
        def inc():
            nonlocal value
            value += step
            return value
        return inc
    
    c1 = make_counter()
    c2 = make_counter(100, 10)
    print(c1(), c1(), c1())      # 1 2 3
    print(c2(), c2())            # 110 120
    

    Замыкания и «позднее связывание»

    Частая ловушка при создании функций в цикле — все они «видят» последнее значение счётчика. Решение — зафиксировать значение аргументом по умолчанию:

    funcs = []
    for i in range(3):
        def f(i=i):  # фиксируем текущее i в значении по умолчанию
            return i
        funcs.append(f)
    
    print([fn() for fn in funcs])  # [0, 1, 2]
    

    Частые ошибки и как их избегать

  • UnboundLocalError: вы присваиваете переменной внутри функции, значит она локальная; чтение до присваивания запрещено. Лечится global, nonlocal или грамотным рефакторингом (передать параметр, вернуть результат).
  • Затенение встроенных имён: не называйте переменные как len, list, sum — вы потеряете доступ к встроенной функции.
  • len = 10
    # print(len([1, 2]))  # TypeError: 'int' object is not callable
    del len                # вернули доступ к встроенной len
    print(len([1, 2]))     # 2
    
  • Глобальные «магические» константы: если нужны действительно глобальные значения — делайте их КОНСТАНТАМИ В ВЕРХНЕМ РЕГИСТРЕ и не меняйте (например, API_URL).
  • Сложность тестирования: функции с побочными эффектами и глобальным состоянием сложнее мокать и покрывать тестами. Делайте зависимости явными (DI через параметры).
  • Чеклист лучших практик

  • Избегайте global — предпочитайте параметры и возврат значений.
  • Для состояния используйте объекты (классы) или замыкания с nonlocal.
  • Не затеняйте встроенные имена и не переиспользуйте «говорящие» идентификаторы.
  • Пишите маленькие, чистые функции без скрытых зависимостей — их легко тестировать и переиспользовать.
  • Если всё же нужен глобальный реестр или кеш — инкапсулируйте доступ через функции/классы, а не меняйте переменные напрямую.
  • Итоги

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

    Хотите быстро и системно прокачать Python с практикой и домашками? Посмотрите программу и первые уроки: Открыть программу курса «Программирование на Python с Нуля до Гуру» — отличный путь от базовых тем до уверенного уровня.

    Источник

    НЕТ КОММЕНТАРИЕВ

    Оставить комментарий