Основы программирования
Область видимости
**Загадка:** Ты создал переменную `x = 10` внутри функции. Потом пытаешься её использовать снаружи - ошибка! Куда она делась? И почему `x = 5` глобально, а внутри функции `x = 10` - это разные x?
Область видимости (scope) - это "территория", где переменная существует и доступна. Понимание scope - ключ к избежанию багов и написанию чистого кода.
Цели урока
- Понять разницу между локальными и глобальными переменными
- Освоить правило LEGB (Local, Enclosing, Global, Built-in)
- Научиться безопасно работать с глобальными переменными
- Понять замыкания (closures)
Предварительные знания
- Функции (урок 8)
- Переменные и типы (урок 3)
В большом проекте тысячи переменных. Если бы все были глобальными - хаос и конфликты имён. Scope - это изоляция, защита и организация кода.
- **Модули**: каждый файл - свой scope
- **Классы**: переменные экземпляра vs класса
- **Многопоточность**: изоляция данных между потоками
- **Безопасность**: ограничение доступа к данным
От блочного scope ALGOL до lexical Scheme, hoisting JS и LEGB Python
Концепция блочной области видимости появилась в ALGOL 60: переменная видна только в том блоке begin...end, где объявлена. До этого в Fortran II и COBOL переменные были глобальными по умолчанию. В 1960-1970-х Lisp использовал dynamic scope: имя ищется по стеку вызовов, и одна функция могла случайно перетереть значение в другой. В 1975 году Джеральд Сассмен и Гай Стил в MIT создали Scheme и выбрали lexical scope: переменная разрешается по тексту программы, а не по тому, кто вызвал функцию. Lexical scope потом победил в Common Lisp (1984), JavaScript, Python и почти везде. JavaScript в 1995 году получил от Брендана Эйка только один способ объявления var, и из-за hoisting переменные поднимались на верх функции, что давало неожиданные баги. ECMAScript 6 в 2015 году добавил let и const с честным блочным scope, и это считается главным фиксом языка за десятилетие. Python с самого начала, с 1991 года, использовал лексический scope, но без блочной видимости в if и for. Только функция, модуль или класс создают новую область. Правило LEGB (Local, Enclosing, Global, Built-in) формализовалось в документации в начале 2000-х, ключевое слово nonlocal для записи в enclosing scope добавлено в Python 3.0 (декабрь 2008, PEP 3104). До этого захват переменной из enclosing scope был только на чтение.
Локальная область видимости
**Локальные переменные** - создаются внутри функции и существуют только там. При выходе из функции они уничтожаются.
Локальная переменная
Живёт только внутри функции
```python def greet(): message = "Привет!" # Локальная переменная print(message) greet() # Привет! print(message) # NameError: name 'message' is not defined ``` Переменная `message` существует только во время выполнения функции.
Параметры - тоже локальные
Они создаются при вызове
```python def add(a, b): # a и b - локальные result = a + b # result тоже локальная return result print(add(3, 5)) # 8 print(a) # NameError - a не существует снаружи ```
**Преимущество локальных переменных:** - Изоляция: не мешают другим частям кода - Память: освобождаются после вызова - Безопасность: нельзя случайно изменить снаружи
Что выведет код? ```python def test(): x = 100 print(x) test() x = 5 print(x) ```
Глобальная область видимости
**Глобальные переменные** - создаются на уровне модуля (вне функций). Видны везде, но изменять их из функции нужно осторожно.
Чтение глобальной переменной
Можно читать без проблем
```python name = "Алиса" # Глобальная def greet(): print(f"Привет, {name}!") # Читаем глобальную - OK greet() # Привет, Алиса! ```
Изменение: ловушка!
Присваивание создаёт локальную
```python counter = 0 # Глобальная def increment(): counter = counter + 1 # Ошибка! # Python думает, что counter локальная (из-за присваивания) # Но она ещё не определена → UnboundLocalError # Правильно: явно указать global def increment_correct(): global counter # Объявляем: использую глобальную counter = counter + 1 increment_correct() print(counter) # 1 ```
**global - плохая практика!** Изменение глобальных переменных делает код непредсказуемым и сложным для тестирования. Лучше передавать данные через параметры и возвращать через return.
global нужен для чтения глобальных переменных
global нужен только для ИЗМЕНЕНИЯ (присваивания) глобальных переменных
Python автоматически ищет переменную в локальном scope, потом в глобальном. Для чтения global не нужен. Он нужен только чтобы сказать: 'не создавай локальную, используй глобальную для записи'.
Когда нужно использовать global?
Правило LEGB
**LEGB** - порядок поиска переменной в Python: Local → Enclosing → Global → Built-in.
LEGB в действии
Четыре уровня scope
```python # Built-in (встроенные): print, len, range... x = "Global" # Global scope def outer(): x = "Enclosing" # Enclosing (внешняя функция) def inner(): x = "Local" # Local scope print(x) # Local inner() print(x) # Enclosing outer() print(x) # Global ```
| Уровень | Где | Пример |
|---|---|---|
| L - Local | Внутри текущей функции | def inner(): x = 1 |
| E - Enclosing | Внешняя функция (для вложенных) | def outer(): x = 2 |
| G - Global | Уровень модуля | x = 3 (вне функций) |
| B - Built-in | Встроенные имена Python | print, len, True |
Поиск по LEGB
Python ищет сверху вниз
```python x = 10 # Global def func(): # x не определена локально # Python ищет дальше → находит в Global print(x) # 10 func() # Но если добавить присваивание: def func2(): print(x) # UnboundLocalError! x = 20 # Это делает x локальной (для всей функции!) ```
**Ловушка:** если где-то в функции есть `x = ...`, то x считается локальной ДЛЯ ВСЕЙ функции, даже для строк до присваивания!
В каком порядке Python ищет переменную?
nonlocal: изменение enclosing
**nonlocal** - как global, но для enclosing scope (переменной из внешней функции).
nonlocal в действии
Изменение переменной внешней функции
```python def outer(): count = 0 def increment(): nonlocal count # Используем переменную из outer count += 1 print(f"Count: {count}") increment() # Count: 1 increment() # Count: 2 increment() # Count: 3 outer() ```
Практика: счётчик-замыкание
Функция "помнит" своё состояние
```python def make_counter(): count = 0 def counter(): nonlocal count count += 1 return count return counter # Создаём счётчик my_counter = make_counter() print(my_counter()) # 1 print(my_counter()) # 2 print(my_counter()) # 3 # Другой счётчик - независимый! other_counter = make_counter() print(other_counter()) # 1 ```
**Замыкание (closure)** - функция, которая "захватывает" переменные из охватывающего scope. Это стандартный паттерн для создания функций с состоянием.
Когда использовать nonlocal вместо global?
Лучшие практики
Правильное использование scope делает код предсказуемым, тестируемым и безопасным.
Плохо: глобальные переменные
Побочные эффекты
```python # ПЛОХО: функция зависит от глобальной переменной user_name = "" def greet(): global user_name print(f"Привет, {user_name}!") user_name = "Алиса" greet() # Непонятно откуда берётся имя # ХОРОШО: явные параметры def greet_better(name): print(f"Привет, {name}!") greet_better("Алиса") # Ясно: имя передано как аргумент ```
Плохо: shadowing (затенение)
Переиспользование имён
```python # ПЛОХО: затенение встроенной функции list = [1, 2, 3] # Теперь list - не функция! print(list([1, 2])) # TypeError: 'list' object is not callable # ПЛОХО: затенение глобальной name = "Глобальное имя" def process(): name = "Локальное" # Затеняет глобальную - путаница! # ... # ХОРОШО: уникальные имена global_name = "Глобальное" def process(): local_name = "Локальное" ```
**Правила чистого кода:** 1. Избегай global - передавай через параметры 2. Не затеняй встроенные имена (list, str, id...) 3. Минимизируй scope переменных 4. Используй говорящие имена
Какой код лучше?
Связь с другими темами
Scope управляет жизнью переменных и связывает функции, замыкания и модули:
- Функции — Каждая функция создаёт собственный локальный scope
- Рекурсия — Каждый рекурсивный вызов получает новый локальный scope
- Массивы и списки — Изменяемые объекты в enclosing scope, замыкания над списками
- Объекты и словари — Атрибуты объекта это ещё одна форма namespace
- Рефакторинг — Сокращение scope и удаление global улучшают тестируемость
Итог
- Блочный scope появился в ALGOL 60, до этого Fortran и COBOL держали переменные глобально
- Sussman и Steele в Scheme (MIT, 1975) выбрали lexical scope вместо dynamic, и это стало стандартом
- JavaScript var с hoisting вызывал баги 20 лет, let и const с блочным scope добавили только в ES6 (2015)
- Python использует правило LEGB: Local, Enclosing, Global, Built-in. Блочную видимость в if/for не создаёт
- nonlocal для записи в enclosing scope добавлен в Python 3.0 (декабрь 2008, PEP 3104), до этого захват был только на чтение
Связанные уроки
- prog-08-functions — Функции задают границы области видимости переменных
- prog-09-recursion — Рекурсивные вызовы хранят каждый свою область
- prog-12-objects — Замыкания захватывают область для объектов с состоянием
- os-07-memory — Стек вызовов хранит локальные области в памяти
- mm-04-second-order
- alg-01-big-o