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

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

**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 ищет его **в таком порядке** и останавливается **на первом месте**, где нашёл:

```text
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
```

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

## Синтаксис

```python
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`:

```python
course = "Kodkod 6"

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

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

**Вывод:**

```text
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
```

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

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

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

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

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

```python
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()
```

**Вывод:**

```text
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`.**

```python
count = 0

def add_one():
    count = count + 1

add_one()
```

```text
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
```

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

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

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

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

```python
def add_one(count):
    return count + 1

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

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

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

```python
list = [1, 2, 3]
numbers = list(range(5))
```

```text
TypeError: 'list' object is not callable
```

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

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

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

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

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

print(welcome("Dana"))
print(message)
```

1. Что произойдёт на последней строке и почему?
2. Исправьте так, чтобы сообщение напечаталось дважды.

{{< solution >}}
1. `NameError: name 'message' is not defined`. `message` — **локальная** (Local) переменная функции `welcome`: она создаётся при каждом вызове и исчезает, когда функция завершается.
2. Сохраните возвращённое значение в собственной переменной:

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


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

```text
Welcome Dana
Welcome Dana
```
{{< /solution >}}

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

```python
x = "global"

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

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

first()
second()
print("3:", x)
```

{{< solution >}}
```python
x = "global"


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


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


first()
second()
print("3:", x)
```

```text
1: local in first
2: global
3: global
```

- `first` создаёт собственную **локальную** `x` — глобальная не тронута.
- `second` не определяет `x`, поэтому по **LEGB** доходим до глобальной (Global).
{{< /solution >}}

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

```python
count = 0

def add_one():
    count = count + 1

add_one()
```

1. Какую ошибку вы получите и почему?
2. Исправьте без `global`.

{{< solution >}}
1. **`UnboundLocalError`**. Из-за присваивания `count = ...` Python решает, что `count` — **локальная** переменная во всей функции, — и затем `count + 1` пытается прочитать локальную, у которой ещё нет значения.
2. Параметр входит, `return` выходит:

```python
def add_one(count):
    return count + 1


count = 0
count = add_one(count)
count = add_one(count)
print(count)
```

```text
2
```
{{< /solution >}}

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

```python
list = [1, 2, 3]
numbers = list(range(5))
```

1. Какую ошибку вы получите?
2. Объясните с помощью LEGB, почему так происходит, и исправьте.

{{< solution >}}
1. `TypeError: 'list' object is not callable`.
2. По **LEGB** Python ищет `list` в **Global** раньше, чем в **Built-in**. Наша переменная с именем `list` «заслонила» встроенную функцию. Исправление — не давать переменным имена встроенных функций:

```python
items = [1, 2, 3]
numbers = list(range(5))
print(items, numbers)
```

```text
[1, 2, 3] [0, 1, 2, 3, 4]
```
{{< /solution >}}

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

```python
cart = []
total = 0

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

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

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

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

```text
[100, 50] 150
```

{{< solution >}}
```python
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))
```

```text
[100, 50] 150
```

- `total` больше не «состояние», которое нужно поддерживать, — оно **вычисляется** из корзины.
- `add` возвращает **новый** список (`cart + [price]`) и не меняет существующий.
{{< /solution >}}

<!-- exercises:end -->

## Итоги

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

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

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

- [Python Scopes and Namespaces](https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces)
- [Resolution of names](https://docs.python.org/3/reference/executionmodel.html#resolution-of-names)
- [The global statement](https://docs.python.org/3/reference/simple_stmts.html#the-global-statement)

<!-- docs:end -->
