Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Область видимости переменных (LEGB)

Понимание локальных, объемлющих, глобальных и встроенных пространств имен и ключевого слова global.

Прогресс урока: 0%

Введение в пространства имен и область видимости

Добро пожаловать на двадцать второй урок нашего углубленного курса по Python! Сегодня мы разбираем одну из самых важных, сложных и фундаментальных тем, которая отличает новичка, пишущего скрипты методом проб и ошибок, от профессионала, осознанно управляющего архитектурой своего приложения. Эта тема — пространства имен (namespaces) и область видимости переменных (scope). Концепция области видимости определяет, в какой части вашего программного кода переменная или имя функции будет доступно для использования. Представьте себе огромную транснациональную корпорацию. У каждого сотрудника есть свое рабочее место, свой отдел, филиал и штаб-квартира. Если вы, находясь в отделе маркетинга, скажете 'передайте документы Анне', ваши коллеги будут искать Анну именно в вашем отделе. Если в вашем отделе ее нет, они обратятся к списку всего филиала, а затем, если потребуется, в глобальный справочник корпорации. В языке Python этот механизм поиска реализован строго и предсказуемо, и он называется правилом LEGB.

Правило LEGB — это аббревиатура, которая расшифровывается как Local (Локальная), Enclosing (Объемлющая), Global (Глобальная) и Built-in (Встроенная) области видимости. Интерпретатор Python всегда, без исключений, ищет запрошенную вами переменную именно в таком иерархическом порядке. Сначала он проверяет локальное пространство имен (внутри текущей функции). Если имя там не найдено, он переходит на уровень выше — в объемлющую область, которая актуальна для вложенных функций (замыканий). Если поиск снова не дал результатов, Python заглядывает в глобальную область модуля. И в самом крайнем случае обращается к встроенным функциям и константам самого языка. Если имя не найдено ни на одном из этих уровней, генерируется исключение NameError.

Пространство имен (namespace) — это, если заглянуть под капот интерпретатора CPython, просто словарь (хэш-таблица), в котором ключами являются имена переменных в виде строк, а значениями — ссылки на объекты в оперативной памяти. У каждого модуля, каждой функции и каждого класса есть свой собственный словарь пространства имен. Когда вы пишете x = 10, Python добавляет ключ 'x' со ссылкой на объект '10' в текущее пространство имен. Понимание того, что области видимости изолированы друг от друга, критически важно. Вы можете иметь десяток переменных с именем result в разных функциях, и они никогда не пересекутся и не перезапишут друг друга, потому что существуют в разных словарях. В этом уроке мы детально, с микроскопической точностью разберем каждую из четырех областей LEGB, научимся работать с ключевыми словами global и nonlocal, а также изучим паттерны проектирования, которые позволят вам избегать типичных ошибок, таких как UnboundLocalError.

Буква Область видимости Описание Пример функции/метода
L Local (Локальная) Имена, определенные внутри текущей функции. Создаются при вызове функции и уничтожаются при возврате. Переменные внутри def
E Enclosing (Объемлющая) Имена в локальной области внешней функции. Доступны для вложенных функций (замыканий). Внешняя def для внутренней def
G Global (Глобальная) Имена, определенные на верхнем уровне текущего модуля (файла) или объявленные через global. Переменные вне всех функций
B Built-in (Встроенная) Имена, предварительно назначенные в модуле builtins. Встроенные функции и исключения. print(), len(), ValueError
python
built_in_var = len  # B - Built-in: функция len()

global_var = "Я глобальная переменная"  # G - Global

def outer_function():
    enclosing_var = "Я объемлющая переменная"  # E - Enclosing
    
    def inner_function():
        local_var = "Я локальная переменная"  # L - Local
        print(local_var)
        print(enclosing_var)
        print(global_var)
        print(built_in_var([1, 2, 3]))
        
    inner_function()

outer_function()

Как интерпретатор обрабатывает код: разрешение имен

Чтобы стать действительно сильным разработчиком, необходимо понимать внутренние процессы интерпретатора. Когда Python встречает имя переменной, например, local_var в функции print(local_var), он начинает процедуру, называемую разрешением имен (name resolution). В этот момент запускается механизм, строго следующий правилу LEGB. Важно понимать разницу между операциями чтения и записи переменных. Правило LEGB описывает исключительно процесс чтения (поиска) переменной. Как только вы пытаетесь присвоить переменной новое значение с помощью оператора = (например, my_var = 5), механизм работает иначе. Если вы присваиваете значение переменной внутри функции, Python автоматически, без дополнительных инструкций, создает эту переменную в локальном пространстве имен (Local), даже если переменная с точно таким же именем уже существует в глобальной области. Это базовая защита от случайного изменения глобального состояния изнутри функций.

Давайте рассмотрим, почему разработчики языка выбрали именно такой подход. В некоторых старых языках программирования все переменные по умолчанию были глобальными. Это приводило к катастрофическим последствиям: вы могли вызвать функцию, которая внутри себя меняла счетчик цикла i, и это ломало внешний цикл, использующий ту же переменную i. В Python философия 'Явное лучше, чем неявное' (The Zen of Python) применяется в полной мере. По умолчанию функции полностью инкапсулированы. Они могут читать глобальные данные (используя LEGB), но не могут их изменять, создавая вместо этого локальные копии имен. Если же разработчику действительно необходимо изменить глобальную переменную из локальной области, он должен явно заявить об этом намерении, использовав специальное ключевое слово global. Аналогично для объемлющих областей используется слово nonlocal.

Для интроспекции (самоанализа кода) в Python существуют встроенные функции globals() и locals(). Они возвращают словари, представляющие текущие пространства имен. Вы можете вывести их на экран с помощью print, чтобы своими глазами увидеть, какие именно переменные существуют в памяти в данный момент времени. Функция globals() вернет словарь со всеми модульными переменными, включая служебные (такие как __name__ или __doc__), а locals() внутри функции вернет словарь локальных переменных. Изучение вывода этих функций — отличный способ для новичков понять, как Python управляет памятью и отследить момент появления и исчезновения переменных при вызове функций.

В каком порядке интерпретатор Python ищет переменную при обращении к ней (например, при попытке вывести её через print)?

Введите аббревиатуру, состоящую из 4 английских букв, которая описывает порядок поиска переменных в пространствах имен Python.

python
x = 100

def check_namespaces():
    y = 200
    # locals() возвращает {'y': 200}
    print("Локальные:", locals())
    # globals() вернет большой словарь, где будет {'x': 100, ...}
    print("Есть ли 'x' в глобальных?", 'x' in globals())

check_namespaces()

Задание

Практическое задание: интроспекция словарей пространств имен. Откройте свою IDE или консоль Python.

  • Определите глобальную переменную `message = 'Hello'`.
  • Напишите функцию `analyzer()`, внутри которой определите локальную переменную `count = 1`.
  • Внутри функции выведите на экран результат вызова `locals()`.
  • За пределами функции выведите на экран результат вызова `globals()`.
  • Сравните полученные словари. Убедитесь, что `count` нет в `globals()`, а `message` нет в `locals()`.
10 баллов

Что содержит словарь, возвращаемый функцией `locals()` при вызове внутри функции?

Глубокое погружение: Локальная область видимости (Local Scope)

Локальная область видимости — это первый уровень иерархии поиска LEGB. Когда вы определяете функцию с помощью ключевого слова def и вызываете ее, интерпретатор Python создает для этого вызова новый, изолированный кадр стека (stack frame). Этот кадр стека содержит свое собственное, независимое пространство имен (словарь локальных переменных). Все переменные, которым присваивается значение внутри тела функции, по умолчанию попадают в этот локальный словарь. Это касается не только переменных, созданных через оператор присваивания =, но и параметров самой функции. Когда вы пишете def my_func(a, b):, параметры a и b автоматически становятся локальными переменными в момент вызова функции, и им присваиваются значения, переданные в качестве аргументов. Важнейшее свойство локальной области видимости заключается в ее эфемерности, то есть временном характере. Как только функция завершает свое выполнение (достигает оператора return или конца блока кода), ее кадр стека уничтожается. Вместе с ним уничтожается и словарь локальных переменных. Объекты, на которые ссылались эти локальные переменные, могут быть удалены сборщиком мусора (Garbage Collector), если на них не осталось других ссылок в программе.

Эта изоляция локальных переменных — краеугольный камень создания модульных, предсказуемых и тестируемых программ. Представьте, если бы функции не имели локальной области видимости: любая переменная result или temp внутри функции перезаписывала бы данные в других частях программы. Благодаря локальной области, функция представляет собой своеобразный 'черный ящик'. Она принимает входные данные (параметры), производит вычисления с использованием своих изолированных временных переменных и возвращает результат, никак не 'загрязняя' внешний мир. Это позволяет разработчикам использовать простые и понятные имена переменных (такие как i, data, index) внутри функций, не боясь конфликтов имен. Тем не менее, эта строгая изоляция иногда приводит к неожиданному поведению для новичков. Самая частая проблема — это попытка изменить переменную, определенную вне функции, без понимания механизма затенения. Если глобальная переменная counter равна 0, а внутри функции вы напишете counter = 1, вы не измените глобальную переменную! Вместо этого Python создаст абсолютно новую локальную переменную с именем counter, которая просто скроет (затенит) глобальную переменную на время работы функции.

Существует также тонкий нюанс, связанный со временем создания локальных переменных. Python — интерпретируемый язык, но он выполняет предварительную компиляцию кода в байт-код перед выполнением. На этапе компиляции функции Python анализирует тело функции. Если он видит оператор присваивания какой-либо переменной (например, x = 10), он заранее помечает имя x как локальную переменную для всей функции целиком, от первой до последней строчки ее тела. Это приводит к известной ошибке UnboundLocalError, если вы попытаетесь прочитать переменную x (ожидая получить глобальное значение) до того, как ей будет присвоено локальное значение внутри функции. Интерпретатор 'знает', что переменная локальная, но она еще не связана со значением (unbound). Этот механизм доказывает, что области видимости определяются лексически (на этапе чтения кода), а не динамически.

python
user_id = 101  # Глобальная переменная

def process_user(user_id):
    # Здесь user_id - это локальная переменная (параметр функции)
    # Она затеняет глобальную переменную user_id
    user_id = user_id + 50
    print("Внутри функции user_id =", user_id)

process_user(500)
print("Снаружи функции user_id =", user_id)

Опасность UnboundLocalError: Почему возникает ошибка?

Ошибка UnboundLocalError: local variable referenced before assignment — это своеобразный обряд инициации для каждого разработчика на Python. Чтобы избежать ее в будущем, нужно понять логику интерпретатора. Давайте разберем классический пример. У нас есть глобальная переменная count = 0. Мы пишем функцию increment(), внутри которой пытаемся увеличить этот счетчик: count = count + 1. Казалось бы, логика железобетонная: возьми глобальный count, прибавь единицу и сохрани обратно. Однако при вызове функции мы получаем фатальную ошибку интерпретатора. Почему? Проблема кроется в механизме определения локальных переменных, о котором мы упоминали ранее. Когда интерпретатор анализирует код функции increment() до ее реального выполнения, он сканирует тело на наличие операторов присваивания. Он видит строчку count = .... В этот момент интерпретатор принимает жесткое решение: 'Ага, переменная count изменяется внутри функции. Следовательно, переменная count во всем теле этой функции является локальной'. Он создает пустой слот для локальной переменной count.

Теперь начинается фактическое выполнение функции. Интерпретатор доходит до выражения count = count + 1. Оператор присваивания вычисляется справа налево. Сначала Python пытается вычислить правую часть: count + 1. Для этого ему нужно узнать текущее значение локальной переменной count. Но подождите! Локальная переменная count уже зарегистрирована как локальная (из-за наличия знака равенства), но ей еще не было присвоено никакого значения (это произойдет только после вычисления правой части). Она пуста, она 'unbound' (несвязанная). Python паникует, так как не может прибавить единицу к пустоте, и выбрасывает UnboundLocalError. Обратите внимание: из-за того, что интерпретатор пометил переменную как локальную, правило поиска LEGB прерывается на букве 'L'. Он даже не пытается искать глобальную переменную count! Он твердо уверен, что вы работаете с локальной переменной, которую просто забыли инициализировать. Это поведение часто кажется нелогичным после других языков программирования (например, JavaScript), но оно обеспечивает строгую инкапсуляцию.

Как же решить эту проблему? Есть два пути. Первый путь (плохой) — использовать ключевое слово global, о котором мы поговорим детально чуть позже. Указав global count внутри функции, вы заставите интерпретатор отключить создание локальной переменной и работать напрямую с глобальным словарем. Второй путь (правильный и Pythonic way) — не изменять глобальные переменные вообще. Вместо этого передайте значение в функцию как аргумент, вычислите новое значение, верните его с помощью return и переопределите глобальную переменную снаружи: count = increment(count). Этот подход соответствует принципам функционального программирования (pure functions), делает код предсказуемым, облегчает написание unit-тестов и полностью избавляет вас от головной боли с областями видимости и загадочными изменениями состояния программы.

python
counter = 0  # Глобальная переменная

def bad_increment():
    # Эта строка вызовет UnboundLocalError
    # counter = counter + 1
    pass

# Правильный подход (Функциональный стиль):
def good_increment(current_value):
    return current_value + 1

counter = good_increment(counter)
print(counter)  # Выведет: 1

Почему в Python возникает ошибка UnboundLocalError при конструкции `x = x + 1`, где `x` определена только в глобальной области?

Какое исключение (ошибку) выбросит интерпретатор, если вы попытаетесь прочитать локальную переменную до того, как ей было присвоено значение внутри функции?

Задание

Задание на рефакторинг: Исправление архитектуры без global.

  • У вас есть код с ошибкой: `total = 100`, функция `def apply_discount(): total = total - 10`.
  • Ваша задача: переписать функцию так, чтобы она принимала аргумент `current_total` и возвращала новое значение.
  • За пределами функции обновите переменную `total`, присвоив ей результат вызова переписанной функции.
10 баллов

Что происходит с локальными переменными после того, как функция завершает свое выполнение (достигает return)?

python
my_list = [1, 2, 3]  # Глобальный изменяемый объект

def mutate_list():
    # Это сработает без ошибок! Мы не переприсваиваем переменную,
    # мы вызываем метод существующего объекта.
    my_list.append(4)
    
mutate_list()
print(my_list)  # Выведет: [1, 2, 3, 4]

Глобальная область видимости (Global Scope) и ключевое слово global

Глобальная область видимости (буква G в LEGB) — это пространство имен, которое принадлежит конкретному модулю (файлу). Переменные, определенные вне всех функций и классов, живут именно здесь. Важно понимать, что в Python 'глобальная' переменная на самом деле является глобальной только в пределах одного файла (модуля). Если у вас есть файл app.py с переменной config = True, эта переменная не будет автоматически доступна в файле utils.py. Чтобы получить к ней доступ из другого модуля, ее нужно явно импортировать (from app import config). Это существенное отличие от некоторых других языков, где глобальные переменные засоряют пространство имен всего проекта целиком. Глобальные переменные в Python часто используются для хранения констант — значений, которые не меняются во время выполнения программы. По общепринятому соглашению (PEP 8), такие константы пишутся заглавными буквами с подчеркиваниями, например: MAX_CONNECTIONS = 100 или DATABASE_URI = 'sqlite://'. Чтение таких глобальных переменных внутри функций абсолютно безопасно и широко практикуется.

Однако, как мы уже выяснили, изменение (переприсваивание) глобальной переменной из локальной области видимости по умолчанию запрещено. Но что делать, если архитектура вашей программы строго требует изменения глобального состояния? Для этого в Python предусмотрено ключевое слово global. Директива global имя_переменной, написанная внутри тела функции, действует как явный приказ интерпретатору: 'Не создавай локальную переменную с этим именем! Вместо этого привяжи это имя напрямую к переменной в глобальном словаре модуля'. Это позволяет не только изменять существующие глобальные переменные (x = x + 1), но и создавать абсолютно новые глобальные переменные прямо изнутри локальной области видимости. Если переменная с указанным именем не существовала до вызова функции, использование global и последующее присваивание создаст ее на уровне модуля. Это мощный инструмент, но его использование связано с огромными рисками для качества кода.

Почему опытные разработчики избегают ключевого слова global как огня? Во-первых, оно нарушает принцип инкапсуляции. Функция перестает быть независимым блоком кода. Если функция меняет глобальное состояние, ее результат начинает зависеть не только от переданных аргументов, но и от порядка вызова других функций в программе. Такой код невозможно тестировать изолированно (unit testing), так как перед каждым тестом придется сбрасывать глобальное состояние. Во-вторых, код становится трудно читаемым (spaghetti code). Если в проекте на тысячи строк десятки функций меняют одну глобальную переменную user_status, при возникновении бага найти виновника будет невероятно сложно. В-третьих, глобальные переменные создают проблемы в многопоточном программировании из-за состояний гонки (race conditions). Использование global оправдано в очень редких случаях: например, в простейших скриптах автоматизации, для кэширования (memoization) на уровне модуля (хотя functools.lru_cache лучше), или при использовании паттерна проектирования 'Одиночка' (Singleton), хотя в Python есть более элегантные способы его реализации.

python
logged_in = False  # Глобальный флаг состояния

def login_user():
    global logged_in  # Явное указание использовать глобальную переменную
    # Без строки выше, это присваивание создало бы локальную переменную
    logged_in = True
    print("Пользователь вошел в систему.")

def check_status():
    # Для ЧТЕНИЯ global не нужен, LEGB найдет переменную
    if logged_in:
        print("Доступ разрешен.")
    else:
        print("Доступ запрещен.")

check_status() # Доступ запрещен
login_user()
check_status() # Доступ разрешен

Тонкости использования ключевого слова global: Мутация против Присваивания

Существует один крайне важный нюанс, который сбивает с толку многих разработчиков, изучающих область видимости в Python. Это разница между переприсваиванием имени (assignment) и изменением состояния объекта (mutation). Чтобы понять это, мы должны вспомнить, что переменные в Python — это не коробки, в которых лежат значения, а просто ярлыки (ссылки), которые привязаны к объектам в оперативной памяти. Когда мы говорим, что функция не может изменить глобальную переменную без ключевого слова global, мы имеем в виду строго операцию связывания ярлыка с новым объектом. Выражение x = 10 означает: 'оторви ярлык x от старого объекта и прикрепи к новому объекту 10'. Именно эту операцию 'переклейки ярлыка' Python запрещает делать с глобальными переменными из локальной области по умолчанию.

Но что, если наш ярлык приклеен к изменяемому (mutable) объекту, например, к списку (list), словарю (dict) или множеству (set)? В этом случае мы можем изменить сам объект, не переклеивая ярлык! Если глобальная переменная — это список data = [1, 2], то внутри функции мы можем вызвать метод data.append(3) или изменить элемент по индексу data[0] = 99. Интерпретатор выполняет разрешение имени по правилу LEGB: он не находит data в локальной области, ищет в глобальной, находит наш список и изменяет его внутреннее состояние. При этом сам ярлык data по-прежнему указывает на тот же самый объект списка в памяти. Операции переприсваивания не было, поэтому Python не пытается создать локальную переменную и не требует ключевого слова global. Это поведение абсолютно корректно и часто используется, но оно требует глубокого понимания ссылочной модели данных.

Этот механизм порождает интересные архитектурные решения. Некоторые программисты, чтобы избежать использования 'грязного' слова global, упаковывают свои глобальные состояния в словари или классы-контейнеры. Например: app_state = {'user_logged': False}. Внутри функции они просто пишут app_state['user_logged'] = True. С технической точки зрения, это изменение словаря, мутация (mutation), и для этого не нужен global. Код работает, ошибка UnboundLocalError не возникает. Однако, с архитектурной точки зрения, это такой же глобальный разделяемый ресурс, и он несет в себе все те же риски: спагетти-код, сложность тестирования и непредсказуемость. Поэтому правильным подходом (Pythonic way) остается передача состояния через аргументы и возврат новых значений из функций, либо использование объектно-ориентированного программирования (ООП), где состояние инкапсулируется внутри атрибутов экземпляра класса (self.state), а методы работают с этим изолированным состоянием.

python
settings = {"theme": "dark", "language": "en"}

def update_settings():
    # Мы не переприсваиваем переменную 'settings',
    # мы изменяем ключ внутри словаря. 
    # Ключевое слово global ЗДЕСЬ НЕ НУЖНО!
    settings["theme"] = "light"
    
    # А вот если бы мы захотели заменить ВЕСЬ словарь новым,
    # тогда global был бы обязателен:
    # global settings
    # settings = {"theme": "light", "language": "ru"}

update_settings()
print(settings)  # Выведет: {'theme': 'light', 'language': 'en'}

В каком из перечисленных случаев вам ОПРЕДЕЛЕННО потребуется использовать ключевое слово `global` внутри функции?

Какое ключевое слово используется для того, чтобы разрешить функции переприсваивать значения переменным, определенным на уровне модуля?

Что произойдет, если использовать `global x` внутри функции, но переменная `x` еще не была определена в коде?

Действие с глобальной переменной Тип данных Нужен ли global?
Чтение (print, if var == ...) Любой Нет (сработает LEGB)
Переприсваивание (var = ...) Неизменяемый (int, str, tuple) Да
Переприсваивание (var = ...) Изменяемый (list, dict) Да
Мутация (var.append(), var['k']=v) Изменяемый (list, dict) Нет

Объемлющая область видимости (Enclosing Scope) и Замыкания

Буква 'E' в правиле LEGB расшифровывается как Enclosing (объемлющая область видимости). Эта концепция вступает в игру исключительно тогда, когда вы используете вложенные функции — то есть определяете одну функцию (inner) внутри другой функции (outer). В Python функции являются объектами первого класса (first-class citizens). Это означает, что функции можно передавать как аргументы, возвращать из других функций и создавать динамически прямо в процессе выполнения кода. Когда внутренняя функция обращается к переменной, интерпретатор сначала ищет ее в локальной области внутренней функции (Local). Если не находит, он поднимается на уровень выше — в локальную область внешней функции. Это и есть та самая 'Объемлющая область' (Enclosing Scope). Если во внешней функции переменной тоже нет, поиск продолжится в глобальной области и так далее. Способность внутренних функций запоминать и использовать переменные из своей объемлющей области лежит в основе одного из самых элегантных паттернов программирования — замыканий (closures).

Замыкание возникает, когда внешняя функция завершает свою работу и возвращает внутреннюю функцию в качестве результата, при этом внутренняя функция 'запоминает' состояние переменных из объемлющей области, даже когда внешняя функция уже удалена из оперативной памяти. Давайте представим фабрику функций. Вы пишете внешнюю функцию multiplier(factor). Внутри нее вы определяете функцию multiply(n), которая возвращает n * factor. Внешняя функция возвращает саму функцию multiply (без круглых скобок, то есть возвращает объект функции, а не вызывает ее). Когда вы вызываете doubler = multiplier(2), вы получаете новую функцию, которая 'запомнила', что её factor равен 2. Локальная переменная factor должна была бы уничтожиться после завершения multiplier, но поскольку возвращенная функция multiply ссылается на нее, Python бережно сохраняет эту переменную в специальном атрибуте замыкания (в словаре __closure__). Это позволяет создавать мощные генераторы функций с привязанным к ним приватным состоянием.

Замыкания часто используются как легкая и быстрая альтернатива классам. Если вам нужно сохранить какое-то состояние между вызовами функции (например, кэшировать предыдущие результаты или считать количество вызовов), но писать полноценный класс с методами __init__ и __call__ кажется избыточным, замыкания — идеальный выбор. Они обеспечивают строгую инкапсуляцию: доступ к переменным объемлющей области имеет только внутренняя функция, и ни одна другая часть программы не может изменить их напрямую. Это делает замыкания излюбленным инструментом в функциональном программировании и при создании декораторов — мощной конструкции Python для модификации поведения функций, которая полностью базируется на механике Enclosing Scope и замыканиях. Однако, так же как и с глобальными переменными, изменение переменных объемлющей области требует особого подхода, который мы рассмотрим далее.

python
def make_multiplier(factor): # Внешняя функция (Enclosing scope)
    # factor живет в объемлющей области для функции inner
    def inner(number):       # Внутренняя функция (Local scope)
        # inner 'видит' и запоминает переменную factor
        return number * factor
    return inner  # Возвращаем саму функцию (создаем замыкание)

# Создаем функцию, которая умножает на 5
times_five = make_multiplier(5)

# Внешняя функция уже завершилась, но times_five помнит factor=5
print(times_five(10))  # Выведет: 50
print(times_five(2))   # Выведет: 10

Ключевое слово nonlocal: Управление состоянием замыкания

Точно так же, как чтение глобальных переменных отличается от их изменения, чтение переменных объемлющей области отличается от их изменения. Если внутренняя функция попытается переприсвоить значение переменной из внешней функции (например, count = count + 1), интерпретатор Python поведет себя стандартным образом: он воспримет это как создание новой локальной переменной для внутренней функции. Это приведет к уже знакомой нам ошибке UnboundLocalError. Для решения этой проблемы в Python 3 было введено специальное ключевое слово — nonlocal. Оно работает по аналогии с global, но с принципиальным отличием: директива nonlocal var_name указывает интерпретатору искать переменную не в глобальной области модуля, а исключительно в локальных областях внешних объемлющих функций. Если переменная там найдена, интерпретатор связывает локальное имя с этой переменной, позволяя изменять ее значение. Если переменной нет ни в одной из объемлющих функций, будет выброшена синтаксическая ошибка (SyntaxError).

Давайте рассмотрим типичный пример использования nonlocal — создание счетчика вызовов. Допустим, мы хотим написать функцию, которая при каждом вызове возвращает число на единицу больше предыдущего. Если использовать глобальную переменную, мы получим спагетти-код и невозможность создать два независимых счетчика. Если использовать классы — код получится слишком громоздким. Замыкание с использованием nonlocal решает эту задачу идеально. Внешняя функция инициализирует count = 0 и возвращает внутреннюю функцию. Внутренняя функция объявляет nonlocal count, затем увеличивает count += 1 и возвращает его. Каждый раз, вызывая внешнюю функцию, мы получаем новый, полностью независимый счетчик со своим собственным изолированным состоянием, запертым внутри замыкания. Это невероятно красивый и эффективный паттерн управления состоянием (State Management).

Существует несколько строгих правил использования nonlocal. Во-первых, вы не можете использовать nonlocal для создания новых переменных в объемлющей области. Переменная обязана уже существовать во внешней функции на момент определения внутренней. В отличие от global, который может создавать глобальные переменные на лету, nonlocal требует явного предварительного объявления (инициализации) переменной. Во-вторых, nonlocal игнорирует глобальную область видимости. Если переменная с указанным именем есть в глобальном пространстве, но ее нет в объемлющих функциях, nonlocal выбросит ошибку. Он ищет строго в пространстве имен (E) правила LEGB. Понимание разницы между global и nonlocal — один из самых частых вопросов на собеседованиях для разработчиков уровня Middle (Intermediate), так как это демонстрирует глубокое понимание архитектуры языка и механизмов работы с памятью.

python
def create_counter():
    count = 0  # Переменная в объемлющей области
    
    def counter():
        nonlocal count  # Разрешаем изменять переменную из внешней функции
        count += 1
        return count
        
    return counter

# Создаем два независимых счетчика
counter_a = create_counter()
counter_b = create_counter()

print("Counter A:", counter_a(), counter_a())  # Выведет: Counter A: 1 2
print("Counter B:", counter_b())               # Выведет: Counter B: 1
print("Counter A:", counter_a())               # Выведет: Counter A: 3

В чем главное отличие ключевого слова `nonlocal` от `global`?

Какое ключевое слово необходимо использовать во внутренней функции (замыкании), чтобы изменить значение переменной, объявленной во внешней функции?

Задание

Практическое применение nonlocal: Создание 'копилки'.

  • Напишите внешнюю функцию `piggy_bank()`, внутри которой определите `balance = 0`.
  • Внутри создайте функцию `add_money(amount)`.
  • Используйте `nonlocal` для изменения `balance`, прибавляя к нему `amount`.
  • Верните из внутренней функции строку: `'Баланс: ' + str(balance)`.
  • В конце внешней функции верните саму функцию `add_money`.
  • Протестируйте, создав объект `my_bank = piggy_bank()` и вызвав `my_bank(100)` пару раз.
10 баллов

Зачем в современных приложениях используют замыкания с `nonlocal`?

Встроенная область видимости (Built-in Scope) и проблема Затенения

Последняя буква в аббревиатуре LEGB — это 'B', что означает Built-in (встроенная область видимости). Это самое широкое и всеобъемлющее пространство имен в Python. Именно здесь 'живут' все встроенные функции языка, такие как print(), len(), sum(), max(), базовые типы данных list, dict, str, int, а также встроенные исключения (ошибки), такие как ValueError или Exception. Технически, эта область видимости представляет собой модуль с названием builtins (в Python 3). Когда вы запускаете свой скрипт, интерпретатор Python автоматически загружает этот модуль в память и делает его содержимое доступным для использования без необходимости писать команду import. Когда вы вызываете len([1, 2, 3]), интерпретатор применяет правило поиска: он ищет функцию len в локальной области (не находит), в объемлющей (не находит), в глобальной (не находит) и, наконец, находит ее во встроенной области.

Эта иерархия поиска таит в себе серьезную опасность, известную как 'затенение' (shadowing). Поскольку Python ищет имена снизу вверх (от локального к встроенному) и останавливается при первом же совпадении, вы можете случайно (или намеренно) создать переменную, имя которой совпадает с именем встроенной функции. Самая распространенная ошибка новичков — назвать список словом list (например, list = [1, 2, 3]) или словарь словом dict. Когда вы делаете это на глобальном или локальном уровне, вы создаете новую переменную, которая затеняет (перекрывает доступ) к оригинальной встроенной функции. Если после такого присваивания вы попытаетесь создать список классическим способом my_new_list = list(some_tuple), Python выдаст ошибку TypeError: 'list' object is not callable. Почему? Потому что имя list теперь ссылается на ваш массив `[1, 2, 3]`, а не на встроенный класс-конструктор! Интерпретатор нашел имя list в глобальной области и даже не стал заглядывать во встроенную область.

Затенение встроенных имен считается грубым нарушением стандартов написания кода (PEP 8) и признаком дурного тона в программировании. Оно приводит к самым трудноуловимым и неожиданным багам, так как встроенный функционал языка перестает работать так, как ожидается. Чтобы избежать этого, современные среды разработки (IDE, такие как PyCharm или VS Code) и линтеры (flake8, pylint) подсвечивают такие имена специальным цветом или выдают предупреждение. Если вам очень нужно использовать имя, похожее на встроенное (например, id пользователя или тип объекта), принято добавлять подчеркивание в конец имени: id_, type_, list_. Если же вы случайно затенили встроенную функцию в интерактивной консоли и хотите вернуть все на свои места, вы можете удалить затеняющую переменную с помощью оператора del (например, del list). После этого имя будет удалено из локального/глобального словаря, и разрешение имен по правилу LEGB снова начнет доходить до встроенной области видимости, восстановив нормальную работу языка.

python
# ОШИБОЧНЫЙ ПАТТЕРН (Затенение built-in):
def calculate_stats(numbers):
    # Создаем локальные переменные, затеняющие встроенные функции!
    min = 0 
    max = 100
    sum = 0
    
    for n in numbers:
        sum += n
        
    # ОШИБКА: попытка вызвать встроенную функцию max(), 
    # но имя 'max' теперь ссылается на число 100!
    # return max(numbers)

# ПРАВИЛЬНЫЙ ПАТТЕРН:
def calculate_stats_correct(numbers):
    min_val = 0   # Изменяем имя переменной
    max_val = 100
    total_sum = 0 # Используем синонимы
    
    # Теперь встроенные функции min, max и sum работают корректно!
    return sum(numbers) / len(numbers)

Почему код `list = [1, 2, 3]; new_lst = list('abc')` вызовет ошибку `TypeError`?

Какой оператор используется для удаления переменной из пространства имен, что позволяет 'вернуть' доступ к затененной встроенной функции? (Введите 3 буквы)

Встроенное имя Чем часто ошибочно затеняют Правильная альтернатива (PEP 8)
list Массив данных (list = [1,2]) lst, items, data_list
dict Словарь с данными (dict = {}) data, config, my_dict
id Идентификатор из базы (id = 5) id_, user_id, record_id
type Тип объекта или события type_, obj_type, kind
dir Путь к директории (dir='/etc') dir_path, folder_path, directory

Продвинутая проблема: Позднее связывание (Late Binding) в замыканиях

Для разработчиков уровня Intermediate существует одна классическая ловушка, связанная с областями видимости и замыканиями, на которой попадаются почти все. Эта проблема известна как 'позднее связывание' (late binding) в генераторах списков или циклах с lambda-функциями. Суть проблемы заключается в том, как Python оценивает (вычисляет) переменные объемлющей области внутри замыкания. Представьте, что вы создаете список из нескольких функций умножения в цикле `for i in range(3):`. На каждом шаге вы добавляете в список небольшую функцию (или лямбду), которая умножает аргумент на `i`. Вы ожидаете получить три разные функции: умножающую на 0, на 1 и на 2. Однако, когда цикл заканчивается, и вы вызываете любую из этих функций, оказывается, что ВСЕ они умножают на 2 (последнее значение счетчика `i`). Эта неочевидная логика часто вызывает ступор.

Почему это происходит? Дело в том, что замыкания (наши функции в списке) не сохраняют копию значения переменной i в момент своего создания. Они сохраняют ссылку на переменную i в объемлющей области видимости. Во время выполнения цикла `for` переменная i постоянно переприсваивается (меняется от 0 до 2). Когда цикл завершается, переменная i в объемлющей области остается равной 2. Поскольку тела функций (включая лямбды) не выполняются (не вычисляются) в момент создания, обращение к `i` происходит только в момент их вызова. И когда мы их вызываем, интерпретатор применяет правило LEGB, находит `i` в объемлющей области и видит, что её текущее (и последнее) значение — 2. Следовательно, все функции используют значение 2. Это называется 'поздним связыванием' (late binding): поиск значения происходит не во время определения (early binding), а во время выполнения функции.

Как обойти эту архитектурную особенность Python? Существует стандартный, 'питонический' хак, основанный на механике аргументов по умолчанию. Мы помним (или должны помнить из уроков по функциям), что аргументы по умолчанию (default arguments) вычисляются ровно один раз — на этапе определения функции (при компиляции байт-кода), а не при её вызове. Поэтому, если мы передадим переменную `i` внутрь замыкания в качестве аргумента по умолчанию (например, lambda x, i=i: x * i), интерпретатор 'заморозит' текущее значение `i` на момент итерации цикла и сохранит его локально для данной функции. Теперь, при вызове функции, она будет использовать свою изолированную локальную переменную-аргумент `i` (которая равна 0, 1 или 2), а не обращаться к объемлющей области. Понимание разницы между поздним связыванием в замыканиях и ранним связыванием в аргументах по умолчанию — признак глубокого понимания семантики языка Python.

python
# ПРОБЛЕМА ПОЗДНЕГО СВЯЗЫВАНИЯ (Late Binding)
multipliers_bad = []
for i in range(3):
    # Замыкание ссылается на переменную i, которая меняется
    multipliers_bad.append(lambda x: x * i)

# Все три функции умножают на 2, так как i после цикла равно 2
print([m(10) for m in multipliers_bad])  # Выведет: [20, 20, 20]

# РЕШЕНИЕ: Использование аргумента по умолчанию (Early Binding)
multipliers_good = []
for i in range(3):
    # i=i замораживает текущее значение на этапе определения функции
    multipliers_good.append(lambda x, i=i: x * i)

print([m(10) for m in multipliers_good]) # Выведет: [0, 10, 20]

Почему все lambda-функции, созданные в цикле `for i in range(5): funcs.append(lambda: i)`, при вызове возвращают 4?

Задание

Задание: Исправьте баг в генераторе кнопок (UI).

  • У вас есть код: `buttons = [lambda: f'Button {n}' for n in range(3)]`.
  • При проверке `buttons[0]()` выдает 'Button 2', а должно 'Button 0'.
  • Перепишите lambda-функцию внутри list comprehension так, чтобы использовать передачу значения по умолчанию `n=n`.
  • Проверьте, что генератор теперь работает корректно (раннее связывание).
10 баллов

Анатомия словарей: Производительность и Области видимости

Достигнув уровня Intermediate, вы должны задумываться не только о том, 'как это работает', но и о том, 'насколько эффективно это работает'. Пространства имен в Python — это словари. Но они не равны по скорости доступа. Поиск переменной в локальной области видимости (L) работает в несколько раз быстрее, чем поиск в глобальной (G) или встроенной (B) области. Это связано с тем, как интерпретатор CPython оптимизирует байт-код. Доступ к локальным переменным внутри функций оптимизируется с использованием массивов фиксированного размера на уровне языка C (ядра интерпретатора). Доступ осуществляется по индексу (смещение в массиве), что является самой быстрой операцией в программировании (O(1) с минимальными накладными расходами). В то же время поиск переменной в глобальной области или в словаре объемлющей функции требует реального хэширования строки имени (поиска ключа в словаре dict). Хотя поиск в словаре тоже выполняется за время O(1), вычисление хэша строки и разрешение коллизий занимает значительно больше процессорных тактов, чем прямое обращение по индексу массива.

Из этой архитектурной особенности вытекает важное правило оптимизации (Performance optimization), которое часто используется в высоконагруженных (HighLoad) системах и математических вычислениях. Если внутри интенсивного цикла (например, на миллион итераций) вы многократно вызываете глобальную или встроенную функцию, например, `math.sqrt()` или метод списка `.append()`, интерпретатор на каждой итерации цикла будет выполнять долгий поиск по правилу LEGB: искать метод в локальной области, не находить, идти в глобальную и т.д. Чтобы избежать этих накладных расходов, опытные разработчики создают локальную ссылку на глобальную функцию перед началом цикла. Например: local_sqrt = math.sqrt. Затем внутри цикла используется имя local_sqrt. В результате интерпретатор находит функцию мгновенно в локальном пространстве, не тратя время на обход словарей LEGB. При миллионах итераций это может ускорить выполнение кода на 15-30%.

Резюмируя сказанное, использование локальных переменных — это не только правильный архитектурный паттерн (чистота кода, инкапсуляция, отсутствие побочных эффектов), но и мощный инструмент повышения производительности. Глобальные переменные плохи не только потому, что они запутывают логику, но и потому, что они медленнее работают. Правило LEGB — это ваш компас. Всегда стремитесь к тому, чтобы ваши переменные находились как можно ближе к месту их использования, то есть в букве 'L'. Используйте 'E' (замыкания) для создания изящных фабрик и декораторов. Оставьте 'G' только для констант конфигурации, а 'B' — для базового инструментария языка, и никогда не затеняйте его.

python
import math
import time

def slow_function():
    # На каждой итерации Python ищет модуль math и функцию sqrt в Global
    result = []
    for i in range(10_000_000):
        result.append(math.sqrt(i))

def fast_function():
    # Мы создаем локальные ссылки! Поиск L -> мгновенно.
    local_append = result.append
    local_sqrt = math.sqrt
    result = []
    for i in range(10_000_000):
        local_append(local_sqrt(i))

# fast_function() выполнится заметно быстрее, так как пропускает 
# этапы E, G и поиск атрибутов объекта в цикле.

Почему доступ к локальным переменным внутри функции работает быстрее, чем доступ к глобальным переменным модуля?

Какую встроенную функцию нужно вызвать без аргументов, чтобы получить текущий словарь глобальных переменных модуля?

Задание

Итоговый микро-проект: Создание 'Умного кэша' (Memoization) с помощью замыканий и nonlocal.

  • Определите внешнюю функцию `memoized_multiply()`, которая не принимает аргументов.
  • Внутри создайте словарь-кэш: `cache = {}`.
  • Создайте внутреннюю функцию `multiply(a, b)`, которая умножает числа.
  • Во внутренней функции создайте ключ: `key = (a, b)`.
  • Проверьте: если `key in cache`, верните значение из кэша и выведите 'Из кэша'.
  • Если нет, вычислите `result = a * b`, сохраните в `cache[key] = result` (здесь nonlocal не нужен, так как это мутация словаря!) и верните результат.
  • Верните внутреннюю функцию из внешней.
10 баллов

У нас есть код: x = 1 def f1(): x = 2 def f2(): nonlocal x x = 3 def f3(): global x x = 4 f3() f2() print(x) f1() print(x) Какие два числа будут выведены на экран по порядку?