Замыкания в 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
def make_counter():
count = 0
def counter():
count += 1
return count
return counter
Проверим чтение: замыкание-проверятель запоминает порог и сравнивает с ним каждое значение — записей в 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 захватывает ровно то, на что внутренняя функция ссылается. Переменные, которые она не упоминает, в замыкание не попадают и умирают вместе с внешним вызовом.
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]())
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. Сколько уровней вложенности мы считаем допустимыми?
Напиши фабрику make_step(step): она возвращает функцию, которая прибавляет step к числу. Создай add_two и add_ten и проверь их вызовы.
Что такое замыкание в Python простыми словами?
Функция, запомнившая переменные из места своего рождения: внутренняя функция уносит с собой переменные внешней и пользуется ими при каждом вызове. Типичный пример — счётчик: замыкание хранит счёт между вызовами, и глобальные переменные для этого не нужны.
Урок 16: декораторыЗачем нужно слово nonlocal?
Чтобы внутренняя функция могла ПЕРЕзаписывать переменную внешней. Чтение работает и без объявления, но присваивание без nonlocal Python считает созданием новой локальной переменной — и код падает с UnboundLocalError при первой же попытке её прочитать.
Чем замыкание лучше глобальной переменной?
Состояние спрятано: до счётчика внутри замыкания нельзя добраться и случайно испортить его из другого места программы. Плюс независимость: два вызова фабрики дают два отдельных счётчика, а глобальная переменная на всех одна.
Почему функции, созданные в цикле, возвращают одинаковый результат?
Замыкание помнит переменную, а не её значение на момент создания: после цикла переменная хранит последнее значение, и все функции видят его. Лечится значением по умолчанию — def add(x, step=n) замораживает значение для каждой функции отдельно.
Что именно захватывает замыкание?
Только те переменные внешней функции, на которые внутренняя ссылается. Переменные, которые она не упоминает, в замыкание не попадают и умирают вместе с внешним вызовом. Проверить состав можно по атрибуту __closure__: по одной ячейке на захваченную переменную.
Как делить состояние между вызовами замыкания?
Объявите переменную внешней функции и записывайте в неё через nonlocal. Тогда каждый вызов внутренней функции видит актуальное значение — как в счётчике make_counter, где count живёт между вызовами counter.
Чем замыкание отличается от класса?
Замыкание — это пара «функция плюс её окружение», а класс — набор данных и методов, работающих с ними. Для двух полей и двух функций замыкание справляется и код выходит короче. Класс нужен, когда данных много, у них есть общие правила или наследование.
Понравился урок? Сошлитесь на него
«Замыкание — это внутренняя функция вместе с переменными внешней функции, которые она запомнила.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Функции · Урок 5
Область видимости в Python: локальное и глобальное
Переменная, созданная внутри функции, снаружи не существует. Разбираем локальные и глобальные имена, ловушку UnboundLocalError и почему global лучше не трогать.
Функции · Урок 13
Функция как значение в Python: передача функций
map и sorted уже принимали функции в аргументах — теперь разберём приём целиком: функция такое же значение, как число или строка.
Функции · Урок 16
Декораторы в Python: обёртка вокруг функции
Собираем декоратор руками: функция-обёртка, знак @, пропуск любых аргументов насквозь и functools.wraps, чтобы функция не теряла имя.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
json · Урок 19
Мини-клиент API на функциях
URL, разбор ответа, фильтр, файл — каждый шаг уже умещается в одну функцию. Собираем их в клиент, который читается как сценарий.
python клиент api функциифункции python для обработки json
ООП · Урок 17
Декораторы: функции над функциями
Декоратор — функция, которая принимает функцию и возвращает новую, добавляя поведение без правки исходного кода. Соберём декоратор с нуля: от замыкания и синтаксиса @ до functools.wraps, декораторов для методов и декораторов с параметрами.
замыкание pythonфункция как аргумент python
Функции · Урок 19
Ошибки функций в Python: TypeError и его друзья
TypeError — короткое письмо о вызове: не хватило аргумента, пришёл лишний, не тот тип. Учимся читать трассировку и ловить ошибки честно.
python typeerror функцияpython ошибки вызова функции