Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 15 из 20 Начальный 20 мин 100 XP

Замыкания в Python: функция с памятью

Функция, которая помнит место своего рождения: делаем счётчик на замыкании, разбираем nonlocal и ловим классическую ловушку с циклом.

Редакция Питоники

Функции до сих пор были исполнителями: вызвали — отработали — забыли всё, что видели. Но иногда функции нужно помнить: счётчику кликов — сколько их было, калькулятору среднего — накопленную сумму, проверятелю возраста — порог, с которым сравнивать. Для этого есть замыкание: функция, которая запомнила переменные из места своего рождения и носит их с собой. На замыканиях держатся счётчики, фабрики настроек и декораторы — тема следующего урока.

Функция внутри функции

Начнём с самого факта: функцию можно объявить внутри другой функции. Внутренняя — обычная функция, просто она живёт в теле внешней и видна только там. А вернуть её наружу можно как значение — вы уже передавали функции как значения.

внешняя функция возвращает внутреннюю
def make_greeter(greeting):
    def greet(name):
        return f'{greeting}, {name}!'
    return greet

hello = make_greeter('Привет')
formal = make_greeter('Добрый день')

print(hello('Анна'))
print(formal('Анна'))
print(hello('Борис'))
Вывод
Привет, Анна!
Добрый день, Анна!
Привет, Борис!

Внешняя функция make_greeter не печатает и не считает — она собирает и возвращает новую функцию greet. Внутри greet используется переменная greeting — аргумент внешней функции. Странная вещь происходит дальше: make_greeter отработала и закончилась, её локальные переменные должны были исчезнуть. Но вызов hello('Анна') сработал — greeting оказался на месте.

Замыкание уносит окружение с собой

Замыкание — это внутренняя функция вместе с переменными внешней функции, которые она запомнила. Python хранит их рядом с функцией, пока функция жива. Переменная при этом не застывшая копия значения, а живая: замыкание может её читать и — с разрешения — менять. Проверим это счётчиком.

счётчик на замыкании
def make_counter():
    count = 0

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

ticks = make_counter()
print(ticks())
print(ticks())
print(ticks())

other = make_counter()
print(other())
print(ticks())
Вывод
1
2
3
1
4

Счётчик ticks наращивал count трижды: единица, двойка, тройка. Затем создали счётчик other — и он пошёл с единицы: у каждого вызова make_counter своё собственное окружение, свои переменные. А ticks своё помнит и после создания other продолжает с четвёрки. Сколько вызовов фабрики — столько независимых замыканий.

nonlocal: разрешение на запись

Читать переменную внешней функции замыкание может свободно. А вот присваивание без предупреждения Python считает созданием новой локальной переменной:

ошибка: присваивание без nonlocal
# Так не запускается: запись во внешнюю переменную без nonlocal
def make_counter():
    count = 0

    def counter():
        count += 1
        return count

    return counter
Python ответит UnboundLocalError: local variable 'count' referenced before assignment. Строка count += 1 — присваивание, поэтому Python заранее объявляет count локальной во внутренней функции, а читать локальную до первой записи нельзя.

Проверим чтение: замыкание-проверятель запоминает порог и сравнивает с ним каждое значение — записей в limit нет, значит, nonlocal не нужен.

замыкание читает порог
def make_checker(limit):
    def check(value):
        return value >= limit
    return check

adult = make_checker(18)
hot = make_checker(25)

print(adult(14))
print(adult(21))
print(hot(21))
Вывод
False
True
False

Порог limit захвачен при создании: adult навсегда сравнивает с восемнадцатью, hot — с двадцатью пятью. Обе функции из одной фабрики, но помнят разное — как ticks и other в счётчике.

Фабрика функций: станок, делающий функции

Приём из примеров зовут фабрикой функций: внешняя функция — станок, внутренняя — деталь со встроенными настройками. Настройки зашиты в замыкание, поэтому готовые функции выглядят обычными: один аргумент, один ответ.

фабрика умножителей
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10), triple(10), double(5))
Вывод
20 30 10

А это бегущее среднее — замыкание хранит сразу две переменные и уточняет ответ с каждым вызовом:

бегущее среднее
def make_average():
    total = 0
    size = 0

    def add(value):
        nonlocal total, size
        total += value
        size += 1
        return total / size

    return add

avg = make_average()
print(avg(4))
print(avg(6))
print(avg(2))
Вывод
4.0
5.0
4.0

Обе переменные — total и size — живут внутри замыкания: снаружи до них не добраться и случайно не испортить. В этом сила приёма — состояние спрятано, наружу торчит только аккуратная функция add. Здесь total и size всегда дают точные деления: 4.0, 5.0, 4.0 — без дробных хвостов.

Ловушка: замыкание помнит переменную, а не значение

У памяти замыкания есть тёмная сторона. Создадим несколько функций-прибавлялок в цикле:

функции из цикла отвечают одинаково
adders = []
for n in [1, 2, 3]:
    def add(x):
        return x + n
    adders.append(add)

print(adders[0](10))
print(adders[1](10))
print(adders[2](10))
Вывод
13
13
13

Все три ответа — тринадцать, хотя функции создавались при разных значениях n. Замыкание запомнило переменную n, а не её значение на момент создания. Цикл прошёл до конца, n осталась тройкой — и все три замыкания смотрят на одну и ту же переменную с её последним значением. Лекарство — заморозить значение в момент создания, поймав его в аргумент по умолчанию:

лечим значением по умолчанию
adders = []
for n in [1, 2, 3]:
    def add(x, step=n):
        return x + step
    adders.append(add)

print(adders[0](10))
print(adders[1](10))
print(adders[2](10))
Вывод
11
12
13

Значение по умолчанию вычисляется один раз, при создании функции, поэтому step у каждой функции свой: единица, двойка, тройка. Тот же механизм вы разбирали в уроке про умолчания — здесь он работает страховкой, а не удобством.

Обычная функция против замыкания

ВопросОбычная функцияЗамыкание
Память о месте рожденияНет: аргументы и глобальныеЕсть: захваченные переменные живут с функцией
Состояние между вызовамиПриносить извнеХранит внутри себя
Два экземпляра одной фабрикиНе различаютсяНезависимые окружения
Запись во внутреннее состояние—Только через nonlocal
  • счётчики и аккумуляторы: состояние прячется внутри замыкания, глобальные переменные не нужны;
  • фабрики настроек: приветствия с разным тоном, проверятели с разными порогами, умножители с разными множителями;
  • короткие правила на месте: lambda из урока про lambda — тоже замыкание, она точно так же помнит переменные внешней функции;
  • декораторы: обёртка вокруг функции — это замыкание, помнящее исходную функцию; этим займётся урок про декораторы.

Замыкание — первый шаг от функций-исполнителей к функциям с внутренним состоянием: одна переменная count, спрятанная в замыкании, заменила глобальный счётчик, который могла бы испортить любая строчка программы. Дальше по курсу — декораторы: они собирают замыкание вокруг вашей функции автоматически.

Что именно попадает в замыкание

Кажется, что замыкание уносит с собой всё, что было во внешней функции. На самом деле Python захватывает ровно то, на что внутренняя функция ссылается. Переменные, которые она не упоминает, в замыкание не попадают и умирают вместе с внешним вызовом.

в замыкание попал только prefix
def make_formatter(prefix):
    unused = 'это значение никому не нужно'

    def format_line(text):
        return f'{prefix}{text}'

    return format_line

f = make_formatter('> ')
print(len(f.__closure__))
print(f('хлеб'))
Вывод
1
> хлеб

В атрибуте __closure__ лежат ячейки — по одной на захваченную переменную. Здесь ячейка одна: только prefix. Переменная unused в замыкание не попала. Это хорошая новость: фабрика не удерживает лишнего, и в замыкание не утекают тяжёлые объекты, о которых inner function забыла.

Замыкание как настройка

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

фабрика прайсера
def make_pricer(currency='руб.', discount=0):
    def price(amount):
        total = amount - amount * discount // 100
        return f'{total} {currency}'
    return price

rubles = make_pricer()
with_card = make_pricer(discount=10)

print(rubles(500))
print(with_card(500))
Вывод
500 руб.
450 руб.

Две функции из одной фабрики, две валюты настройки и два правила скидки. Код, который их вызывает, не знает ни про какие скидки — он просто спрашивает цену у того, кто её умеет считать. Так устроены и настоящие прайсеры в интернет-магазинах: правила меняются в конфиге, а вызывающий код остаётся прежним.

Сколько уровней вложенности допустимо

ГлубинаЧто этоКак читать
Один уровеньфункция с внутренней функциейнорма — это и есть замыкание
Два уровнявнутренняя вызывает ещё внутреннюютерпимо, если имена понятные
Три и большефабрика фабриклучше разбить на отдельные функции

Мы держимся правила: глубже двух уровней вложенности не лезем. Если внутри внутренней функции понадобилась ещё и своя внутренняя — это сигнал, что часть логики просится в отдельную функцию. Иначе через полгода в коде будет тройное def, и разобраться в нём можно будет только с карандашом.

Две функции, одно состояние

Замыкание умеет больше, чем прятать одну переменную. Внутри внешней функции можно объявить несколько внутренних — и все они будут работать с одними и теми же захваченными данными. Получается маленький объект без единого класса.

счёт и пополнение
def make_account(start=0):
    balance = start

    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance

    def current():
        return balance

    return deposit, current

add, get = make_account(100)
print(add(50))
print(get())
print(get())
Вывод
150
150
150

Две внутренние функции делят одну переменную balance — и это их общее состояние. Фабрика вернула обе, и теперь их можно раздать по разным местам программы: add уедет в обработчик платежа, get — в отчёт. Состояние при этом осталось одно и спрятано от всех. Так строят маленькие объекты без единого класса — в Python это вполне рабочий подход, особенно когда класс нужен только ради пары полей и пары методов.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def make_counter():
    count = 0
    def counter():
        nonlocal count
        count += 1
        return count
    return counter

a = make_counter()
b = make_counter()
print(a())
print(a())
print(b())
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(triple(2)))
funcs = []
for n in [1, 2, 3]:
    funcs.append(lambda: n)
print(funcs[0](), funcs[2]())
Проверь себя
0 / 9

1. Что такое замыкание?

2. Внутренняя функция хочет перезаписать count внешней: count += 1. Что нужно?

3. Сколько независимых счётчиков создадут два вызова make_counter()?

4. Функции созданы в цикле for n in [1, 2, 3] и возвращают x + n. Что вернёт первая из них на входе 10?

5. Как заморозить значение переменной цикла при создании функции в цикле?

6. Может ли замыкание читать переменную внешней функции без nonlocal?

7. Что вернёт len(f.__closure__), если замыкание захватило ровно одну переменную?

8. Во внутренней функции написано count = 0 без nonlocal. Что изменится во внешней переменной count?

9. Сколько уровней вложенности мы считаем допустимыми?

Карточки терминов
Запомнено: 0 / 9
Практика

Напиши фабрику make_step(step): она возвращает функцию, которая прибавляет step к числу. Создай add_two и add_ten и проверь их вызовы.

practice.py
Вопросы и ответы по уроку

Что такое замыкание в Python простыми словами?

Функция, запомнившая переменные из места своего рождения: внутренняя функция уносит с собой переменные внешней и пользуется ими при каждом вызове. Типичный пример — счётчик: замыкание хранит счёт между вызовами, и глобальные переменные для этого не нужны.

Урок 16: декораторы

Зачем нужно слово nonlocal?

Чтобы внутренняя функция могла ПЕРЕзаписывать переменную внешней. Чтение работает и без объявления, но присваивание без nonlocal Python считает созданием новой локальной переменной — и код падает с UnboundLocalError при первой же попытке её прочитать.

Чем замыкание лучше глобальной переменной?

Состояние спрятано: до счётчика внутри замыкания нельзя добраться и случайно испортить его из другого места программы. Плюс независимость: два вызова фабрики дают два отдельных счётчика, а глобальная переменная на всех одна.

Почему функции, созданные в цикле, возвращают одинаковый результат?

Замыкание помнит переменную, а не её значение на момент создания: после цикла переменная хранит последнее значение, и все функции видят его. Лечится значением по умолчанию — def add(x, step=n) замораживает значение для каждой функции отдельно.

Что именно захватывает замыкание?

Только те переменные внешней функции, на которые внутренняя ссылается. Переменные, которые она не упоминает, в замыкание не попадают и умирают вместе с внешним вызовом. Проверить состав можно по атрибуту __closure__: по одной ячейке на захваченную переменную.

Как делить состояние между вызовами замыкания?

Объявите переменную внешней функции и записывайте в неё через nonlocal. Тогда каждый вызов внутренней функции видит актуальное значение — как в счётчике make_counter, где count живёт между вызовами counter.

Чем замыкание отличается от класса?

Замыкание — это пара «функция плюс её окружение», а класс — набор данных и методов, работающих с ними. Для двух полей и двух функций замыкание справляется и код выходит короче. Класс нужен, когда данных много, у них есть общие правила или наследование.

Понравился урок? Сошлитесь на него

«Замыкание — это внутренняя функция вместе с переменными внешней функции, которые она запомнила.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.