Область видимости: Local, Enclosing, Global и Built-in (LEGB)

Цели урока
  • Объяснить, что такое Scope и почему переменная, созданная в функции, не существует вне её
  • Знать четыре области видимости — Local, Enclosing, Global и Built-in — и порядок поиска LEGB
  • Понимать, что происходит при изменении глобальной переменной внутри функции и что делает global
  • Писать функции, которые получают данные через параметры и возвращают результат, а не полагаются на глобальные переменные

Что мы изучим?

Scope (область видимости) = область кода, где известно имя (переменной или функции). Мы изучим:

  • Local — переменные внутри функции.
  • Enclosing — переменные внешней функции, когда есть функция внутри функции.
  • Global — переменные на верхнем уровне файла.
  • Built-in — встроенные имена Python (print, len…).
  • LEGB — порядок, в котором Python ищет имя.

Зачем это нужно?

Если бы каждая переменная была доступна отовсюду, переменная total в одной функции перезаписывала бы переменную total в другой, и в программе на 10 000 строк был бы хаос. Область видимости изолирует каждую функцию: она может использовать свои имена, не боясь столкновений.

С другой стороны, ошибки вроде NameError или UnboundLocalError очень запутывают, если не понимать области видимости.

Объяснение с основ

Local

Переменная, созданная внутри функции (включая её параметры), — локальная:

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

Global

Переменная, созданная на верхнем уровне файла (не внутри функции), — глобальная:

  • Она доступна для чтения из любой функции файла.
  • Чтобы изменить её (присвоить новое значение) изнутри функции, нужно явное объявление: global name.

Enclosing

Можно определить функцию внутри функции. Внутренняя видит переменные внешней. Область видимости внешней с точки зрения внутренней называется Enclosing (объемлющая).

Built-in

Имена, которые Python знает всегда, без import: print, len, sum, max, int, str, list, range…

Порядок поиска: LEGB

Когда код использует имя, Python ищет его в таком порядке и останавливается на первом месте, где нашёл:

L  Local       → inside the current function
E  Enclosing   → inside outer functions (if nested)
G  Global      → top level of the file
B  Built-in    → Python's built-in names
              → not found anywhere: NameError
Аналогия: где ключи?

Вы ищете ключи: сначала в кармане (Local), потом в сумке, которую несёте (Enclosing), потом дома (Global) и наконец в вестибюле дома, где вещи оставляют для всех (Built-in). Как только нашли — перестаёте искать. А ключ в кармане друга вы вообще не видите: это Local другой функции.

Синтаксис

counter = 0                  # global

def outer():
    message = "hi"           # local to outer = enclosing for inner

    def inner():
        result = message + "!"   # local to inner; message found in Enclosing
        return result

    return inner()

def increase():
    global counter           # declare: assignments go to the GLOBAL counter
    counter = counter + 1

Первый пример кода

week-2/scope_demo.py:

course = "Kodkod 6"

def welcome(name):
    message = "Welcome " + name + " to " + course
    return message

print(welcome("Dana"))
print(course)
print(message)

Вывод:

Welcome Dana to Kodkod 6
Kodkod 6
Traceback (most recent call last):
  File "scope_demo.py", line 11, in <module>
    print(message)
NameError: name 'message' is not defined

Разбор кода построчно

ИмяОбласть видимостиОбъяснение
courseGlobalСоздано на верхнем уровне. welcome читает его: L (не найдено) → E (нет) → G (найдено!).
nameLocal функции welcomeПараметр = локальная переменная. Существует только во время вызова.
messageLocal функции welcomeСоздаётся внутри функции и исчезает, когда она возвращает значение.
print, len…Built-inНаходятся в конце поиска.
print(message) снаружи—На верхнем уровне нет message. Она была Local и уже исчезла, поэтому NameError.

Правильный способ получить значение из функции — return, и именно это делает welcome. Вызывающий код получил значение (и может сохранить его в своей переменной).

Ещё один пример

Одно и то же имя в нескольких областях видимости и global. week-2/scope_levels.py:

total = 100

def local_total():
    total = 5
    print("inside local_total:", total)

def read_global():
    print("inside read_global:", total)

def change_global():
    global total
    total = total + 50
    print("inside change_global:", total)

def outer():
    label = "outer"

    def inner():
        print("inner sees enclosing label:", label)

    inner()

local_total()
print("after local_total:", total)
read_global()
change_global()
print("after change_global:", total)
outer()

Вывод:

inside local_total: 5
after local_total: 100
inside read_global: 100
inside change_global: 150
after change_global: 150
inner sees enclosing label: outer

Разбор:

  • local_total — присваивание total = 5 создаёт новую локальную переменную с именем total, которая «заслоняет» глобальную. Глобальная не изменилась (100).
  • read_global — только читает total. Локальной с таким именем у неё нет, поэтому по LEGB доходим до глобальной.
  • change_global — global total говорит Python: «в этой функции total — глобальная». Теперь присваивание меняет глобальную (150).
  • inner видит label функции outer. Это E в LEGB.

Типичная ошибка

Ошибка 1: UnboundLocalError — прочитать, а затем изменить глобальную без global.

count = 0

def add_one():
    count = count + 1

add_one()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Почему? Поскольку в функции есть присваивание count, Python решает (заранее, до выполнения), что count — локальная во всей функции. А затем count + 1 пытается прочитать локальную, которая ещё не получила значения.

Ошибка 2 (главная): использовать global как постоянное решение. global «работает», но:

  • Любая функция может изменить переменную, и когда возникает баг, невозможно понять, кто её изменил.
  • Функция зависит от чего-то «снаружи», поэтому её трудно тестировать и переиспользовать.

Чистый способ: параметр входит, return выходит.

def add_one(count):
    return count + 1

count = 0
count = add_one(count)
print(count)   # 1

Теперь совершенно ясно, что входит и что выходит, и функция не трогает ничего, чего не получила. (В теме ООП мы изучим ещё один способ управлять меняющимся «состоянием»: объекты.)

Ошибка 3: перезаписать встроенное имя.

list = [1, 2, 3]
numbers = list(range(5))
TypeError: 'list' object is not callable

Наша глобальная list идёт в LEGB раньше Built-in, поэтому она «заслонила» встроенную функцию. То же самое с sum = 0, max = 10, input = ..., str = .... Никогда не называйте переменные именами встроенных функций.

Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

Упражнение 1: Переменная, которая исчезла

def welcome(name):
    message = "Welcome " + name
    return message

print(welcome("Dana"))
print(message)
  1. Что произойдёт на последней строке и почему?
  2. Исправьте так, чтобы сообщение напечаталось дважды.
Показать решение
  1. NameError: name 'message' is not defined. message — локальная (Local) переменная функции welcome: она создаётся при каждом вызове и исчезает, когда функция завершается.
  2. Сохраните возвращённое значение в собственной переменной:
def welcome(name):
    message = "Welcome " + name
    return message


result = welcome("Dana")
print(result)
print(result)
Welcome Dana
Welcome Dana

Упражнение 2: Предскажите вывод

x = "global"

def first():
    x = "local in first"
    print("1:", x)

def second():
    print("2:", x)

first()
second()
print("3:", x)
Показать решение
x = "global"


def first():
    x = "local in first"
    print("1:", x)


def second():
    print("2:", x)


first()
second()
print("3:", x)
1: local in first
2: global
3: global
  • first создаёт собственную локальную x — глобальная не тронута.
  • second не определяет x, поэтому по LEGB доходим до глобальной (Global).

Упражнение 3: UnboundLocalError

count = 0

def add_one():
    count = count + 1

add_one()
  1. Какую ошибку вы получите и почему?
  2. Исправьте без global.
Показать решение
  1. UnboundLocalError. Из-за присваивания count = ... Python решает, что count — локальная переменная во всей функции, — и затем count + 1 пытается прочитать локальную, у которой ещё нет значения.
  2. Параметр входит, return выходит:
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
2

Упражнение 4: Перезапись встроенного имени

list = [1, 2, 3]
numbers = list(range(5))
  1. Какую ошибку вы получите?
  2. Объясните с помощью LEGB, почему так происходит, и исправьте.
Показать решение
  1. TypeError: 'list' object is not callable.
  2. По LEGB Python ищет list в Global раньше, чем в Built-in. Наша переменная с именем list «заслонила» встроенную функцию. Исправление — не давать переменным имена встроенных функций:
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
[1, 2, 3] [0, 1, 2, 3, 4]

Упражнение 5: Без global

cart = []
total = 0

def add(price):
    global total
    cart.append(price)
    total = total + price

add(100)
add(50)
print(cart, total)

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

Ожидаемый вывод:

[100, 50] 150
Показать решение
def add(cart, price):
    return cart + [price]


def cart_total(cart):
    return sum(cart)


cart = []
cart = add(cart, 100)
cart = add(cart, 50)
print(cart, cart_total(cart))
[100, 50] 150
  • total больше не «состояние», которое нужно поддерживать, — оно вычисляется из корзины.
  • add возвращает новый список (cart + [price]) и не меняет существующий.

Итоги

  • Scope = где известно имя. Переменная, созданная в функции (включая параметры), — Local и исчезает в конце вызова.
  • LEGB: Local → Enclosing → Global → Built-in. Остановка на первом месте, где найдено. Если не найдено — NameError.
  • Присваивание внутри функции создаёт локальную переменную, даже если есть глобальная с тем же именем.
  • global x позволяет менять глобальную изнутри функции, но этого лучше избегать. Параметр входит, return выходит.
  • Не давайте переменным имена встроенных функций (list, sum, max, str…).

Официальная документация