Замыкания
Теория
Функцию можно определить внутри другой функции и вернуть её наружу. Внутренняя функция при этом запоминает переменные внешней функции — даже после того, как внешняя закончила работу. Такая функция вместе с запомненными переменными называется замыканием. Это способ создавать «настроенные» функции: make_discount(10) возвращает функцию, которая всегда применяет скидку 10 %, а make_discount(25) — другую, со скидкой 25 %.
Замыкание запоминает не значение, а саму переменную. Отсюда две особенности. Чтобы изменить запомненную переменную (например, счётчик), во внутренней функции нужен nonlocal — иначе присваивание создаст локальное имя (вспомните UnboundLocalError из прошлой темы). А если замыкания создаются в цикле, все они видят последнее значение переменной цикла — классическая ловушка «позднего связывания».
Наглядно
Таблица 1. Что делает замыкание и как с ним работать.
| Ситуация | Пример | Что важно |
|---|---|---|
| фабрика функций | ten = make_discount(10) |
каждая созданная функция помнит свой percent |
| состояние между вызовами | счётчик номеров заказов | изменение запомненной переменной — через nonlocal |
| замыкания в цикле | lambda price: price * (100 - p) // 100 для каждого p |
все функции видят последнее p; фиксируйте значение параметром по умолчанию или фабрикой |
| чтение без изменения | return price * (100 - percent) // 100 |
nonlocal не нужен |
На практике
Задача: создавать функции скидок под разные акции, нумеровать заказы без глобальной переменной и собрать набор скидок для промо-страницы.
Шаг 1. Определяем функцию внутри функции
Внутренняя функция видит переменные внешней:
def make_receipt(order_id): def line(text): return f"Заказ №{order_id}: {text}" return line("принят") print(make_receipt(1042))
Результат:
Заказ №1042: принят
Шаг 2. Возвращаем функцию наружу
Фабрика скидок возвращает саму внутреннюю функцию — без вызова:
def make_discount(percent): def apply(price): return price * (100 - percent) // 100 return apply ten = make_discount(10) twenty_five = make_discount(25) print(ten(1000), twenty_five(1000))
Результат:
900 750
make_discount уже завершилась, а ten по-прежнему помнит percent = 10, а twenty_five — свой percent = 25.
Шаг 3. Храним состояние между вызовами
Счётчик номеров заказов: запомненная переменная меняется при каждом вызове, поэтому нужен nonlocal:
def make_order_numbers(start): current = start def next_number(): nonlocal current current += 1 return current return next_number next_order = make_order_numbers(1041) print(next_order(), next_order(), next_order())
Результат:
1042 1043 1044
Без nonlocal строка current += 1 сделала бы current локальным — UnboundLocalError.
Шаг 4. Создаём независимые экземпляры
Каждый вызов фабрики даёт своё замыкание со своим состоянием:
shop_orders = make_order_numbers(1000) market_orders = make_order_numbers(5000) print(shop_orders(), shop_orders(), market_orders())
Результат:
1001 1002 5001
Шаг 5. Попадаем в ловушку цикла
Создаём функции скидок для трёх акций в цикле:
discounts = [] for p in (10, 20, 30): discounts.append(lambda price: price * (100 - p) // 100) print([apply(1000) for apply in discounts])
Результат:
[700, 700, 700]
Все три функции видят одну переменную p, а к моменту вызова в ней последнее значение — 30.
Шаг 6. Фиксируем значение
Значение параметра по умолчанию вычисляется при создании функции — так каждая получает своё p:
discounts = [] for p in (10, 20, 30): discounts.append(lambda price, p=p: price * (100 - p) // 100) print([apply(1000) for apply in discounts]) discounts = [make_discount(p) for p in (10, 20, 30)] print([apply(1000) for apply in discounts])
Результат:
[900, 800, 700] [900, 800, 700]
Второй вариант — фабрика: каждый вызов make_discount создаёт своё замыкание со своим percent. Он читается понятнее.
Частые ошибки
- Изменение без
nonlocal.count += 1во внутренней функции —UnboundLocalError. Для изменения запомненной переменной —nonlocal count. - Позднее связывание в цикле. Функции, созданные в цикле, видят последнее значение переменной цикла. Фиксируйте значение:
lambda x, p=p: ...или фабрика. - Вызов вместо возврата.
return apply(price)вместоreturn apply— фабрика вернёт число, а не функцию. - Один экземпляр на всех. Если счётчик нужен на каждый магазин, фабрику вызывают для каждого магазина, а не делят одно замыкание.
Итог
Замыкание
- Внутренняя функция, которая помнит переменные внешней после её завершения.
- Фабрика: внешняя функция возвращает внутреннюю без скобок —
return apply.
Состояние
- Чтение запомненной переменной — без ограничений; изменение — через
nonlocal. - Каждый вызов фабрики — новое независимое замыкание.
Ловушка цикла
- Замыкание запоминает переменную, а не значение: в цикле все видят последнее значение.
- Решения — параметр по умолчанию
p=pили фабрика.
Вопросы на собеседовании
Что такое замыкание?
Функция, которая использует переменные из охватывающей функции и сохраняет к ним доступ после того, как охватывающая функция завершилась. Пример — фабрика make_discount(percent), возвращающая функцию apply(price).
Что выведет [f() for f in [lambda: i for i in range(3)]] и почему?
[2, 2, 2]. Все lambda ссылаются на одну переменную i, а вызываются после цикла, когда в ней последнее значение. Это «позднее связывание»; исправление — lambda i=i: i.
Зачем нужен nonlocal?
Чтобы присваивать переменную охватывающей функции из вложенной. Без него присваивание создаёт новую локальную переменную, а x += 1 падает с UnboundLocalError.
Глубже
Где замыкание хранит запомненные переменные (ячейки __closure__) и почему позднее связывание работает именно так — в курсе «Python под капотом», тема «Замыкания изнутри».