Замыкания

Теория

Функцию можно определить внутри другой функции и вернуть её наружу. Внутренняя функция при этом запоминает переменные внешней функции — даже после того, как внешняя закончила работу. Такая функция вместе с запомненными переменными называется замыканием. Это способ создавать «настроенные» функции: 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. Определяем функцию внутри функции

Внутренняя функция видит переменные внешней:

python
def make_receipt(order_id):
    def line(text):
        return f"Заказ №{order_id}: {text}"
    return line("принят")

print(make_receipt(1042))

Результат:

вывод
Заказ №1042: принят

Шаг 2. Возвращаем функцию наружу

Фабрика скидок возвращает саму внутреннюю функцию — без вызова:

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

python
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. Создаём независимые экземпляры

Каждый вызов фабрики даёт своё замыкание со своим состоянием:

python
shop_orders = make_order_numbers(1000)
market_orders = make_order_numbers(5000)
print(shop_orders(), shop_orders(), market_orders())

Результат:

вывод
1001 1002 5001

Шаг 5. Попадаем в ловушку цикла

Создаём функции скидок для трёх акций в цикле:

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

python
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 под капотом», тема «Замыкания изнутри».

Ошибка в тексте

Сообщить об ошибке могут зарегистрированные пользователи — так в сообщениях меньше спама.

Войти, чтобы сообщить