Основы программирования

Область видимости

**Загадка:** Ты создал переменную `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Встроенные имена Pythonprint, 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
Область видимости

0

1

Войти