Замыкания и lambda
Вложенные функции
Функцию можно объявить внутри другой функции. Внутренняя видит переменные внешней:
def greet(name):
def make_message():
return f"Привет, {name}!" # name из объемлющей функции
return make_message()
print(greet("Аня"))
Замыкание
Если внешняя функция возвращает внутреннюю, та продолжает «помнить» переменные внешней, даже когда внешняя уже завершилась:
def make_power(exp):
def power(base):
return base ** exp
return power
square = make_power(2)
cube = make_power(3)
print(square(5), cube(2))
print(square.__closure__, square.__closure__[0].cell_contents)
print(square.__code__.co_freevars)
Нажмите 👣 Пошагово: make_power завершается, её кадр исчезает, но функция power хранит ссылку на exp.
Замыкание = функция + ссылки на переменные объемлющей области (они хранятся в ячейках, __closure__). Такие переменные называются свободными (co_freevars).
Зачем замыкания
- Фабрики функций: настроить функцию заранее (
make_power(2)). - Состояние без класса: счётчики, кеши, аккумуляторы (с
nonlocal). - Декораторы — целиком построены на замыканиях (следующие уроки).
- Колбэки с контекстом:
button.on_click(make_handler(user_id)).
def make_counter(start=0):
count = start
def counter():
nonlocal count
count += 1
return count
return counter
clicks = make_counter()
views = make_counter(100)
print(clicks(), clicks(), views(), clicks())
Позднее связывание — ловушка
Замыкание хранит ссылку на переменную, а не на её значение. Значение читается в момент вызова:
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] — к моменту вызова i == 2
# Исправление 1: значение по умолчанию вычисляется при создании функции
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])
# Исправление 2: functools.partial
from functools import partial
def show(i):
return i
funcs = [partial(show, i) for i in range(3)]
print([f() for f in funcs])
На собеседовании
«Что выведет [f() for f in [lambda: i for i in range(3)]]?» — [2, 2, 2]. Объясните: замыкание захватывает переменную, а не значение; поиск значения происходит при вызове. Исправление — lambda i=i: i или partial.
lambda
lambda параметры: выражение создаёт функцию из одного выражения:
add = lambda a, b: a + b
print(add(2, 3), add.__name__)
print(sorted(["бб", "а", "ввв"], key=lambda s: len(s)))
print(list(map(lambda x: x ** 2, [1, 2, 3])))
print((lambda x: "чёт" if x % 2 == 0 else "нечет")(7))
Ограничения: одно выражение — нет присваиваний (кроме :=), return, циклов, try; нет docstring; в трассировке ошибок имя <lambda>.
Когда lambda уместна
Как короткий аргумент: key=, map, filter, колбэки. Если функции нужно имя — пишите def. PEP 8 прямо не рекомендует f = lambda x: ....
Итог
- Вложенная функция видит переменные объемлющей; если её вернуть — получится замыкание.
- Замыкание хранит ссылки на переменные в ячейках
__closure__; изменять их — черезnonlocal. - Позднее связывание: значение читается при вызове; исправление —
x=xилиpartial. lambda— функция из одного выражения для коротких аргументов.
Практика решено 0 из 5
Напишите фабрику make_multiplier(n), которая возвращает функцию, умножающую аргумент на n.
double = make_multiplier(2); double(21) → 42.
Какие тесты проверяют код (3)
assert make_multiplier(2)(21) == 42 triple = make_multiplier(3); assert [triple(x) for x in range(4)] == [0, 3, 6, 9] assert make_multiplier(2).__closure__ is not None, "Функция должна быть замыканием"
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
Что выведет код?
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])
Исправьте функцию make_adders(): она должна вернуть список из трёх функций,
где k-я функция прибавляет k к аргументу (adders[0](10) == 10, adders[2](10) == 12).
Какие тесты проверяют код (1)
a = make_adders(); assert [f(10) for f in a] == [10, 11, 12]
def make_adders():
adders = []
for k in range(3):
adders.append(lambda x, k=k: x + k)
return adders
Напишите функцию make_averager(), которая возвращает функцию avg(x):
каждый вызов добавляет число к истории и возвращает среднее всех переданных чисел.
Какие тесты проверяют код (1)
a = make_averager(); assert [a(10), a(20), a(30)] == [10.0, 15.0, 20.0]
def make_averager():
total = 0
count = 0
def avg(x):
nonlocal total, count
total += x
count += 1
return total / count
return avg
Что верно про lambda?
Войдите, чтобы отмечать пройденные уроки.