Замыкания
Вы уже видели функцию как записку с правилом, которую отдают другой функции. Теперь функция сама будет создавать такие функции — и давать им с собой нужные данные.
Представьте, что внешняя функция собирает вложенную в дорогу. Она кладёт в её рюкзак свои переменные, и вложенная функция уходит с ним. Внешняя функция давно закончила работу, а рюкзак остаётся при вложенной.
Нужны функции, которые умножают число на 2, на 3 и на 10. Три отдельных def отличаются одним числом. Вместо них можно написать функцию, которая создаёт такую функцию по числу и возвращает её:
def make_multiplier(n):
def multiply(x):
return x * n
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5)) # 10 15
print(make_multiplier(10)(4)) # 40Давайте разберём пример по строкам:
make_multiplier(2)создаёт новую функциюmultiplyи возвращает её. Она попадает вdouble.make_multiplier(3)создаёт ещё однуmultiply, она попадает вtriple.double(5)умножает5на2, аtriple(5)— на3, поэтому печатается10 15.make_multiplier(10)(4)делает два вызова подряд: первый возвращает функцию, второй сразу вызывает её с4.
Вложенная функция
def внутри тела другой функции создаёт вложенную функцию. Эта строка выполняется при каждом вызове make_multiplier, и каждый раз появляется новая функция multiply.
Имя multiply локальное: снаружи его нет, как нет локальных переменных. Строка return multiply возвращает саму функцию, без скобок, поэтому double — это функция, и её можно вызвать. Вложенную функцию можно вызвать и внутри внешней, как любую другую, но здесь её возвращают.
Замыкание
Локальные переменные исчезают, когда функция возвращает результат. Но double(5) вызывается уже после того, как make_multiplier(2) завершилась, и всё равно видит n, равное 2. Вложенная функция, которая использует переменные внешней функции, хранит их у себя — в том самом рюкзаке. Такую функцию называют замыканием.
Каждый вызов make_multiplier создаёт свою переменную n и своё замыкание. Поэтому у double своё n, равное 2, а у triple — равное 3, и одна функция не меняет другую.
Этот вопрос часто задают на собеседовании: «Что такое замыкание?» Ответ из двух частей:
- это вложенная функция, которая использует переменные внешней функции;
- эти переменные живут, пока живёт замыкание, даже после возврата из внешней функции.
nonlocal
Читать переменную внешней функции замыкание может без подготовки. Чтобы присвоить ей новое значение, во вложенной функции пишут nonlocal и имя. Так же работает global для глобальной переменной:
def make_total():
total = 0
def add(x):
nonlocal total
total += x
return total
return add
add = make_total()
print(add(5), add(3)) # 5 8
print(make_total()(10)) # 10total = 0 выполняется один раз, при вызове make_total, а не при каждом вызове add. Поэтому add копит сумму: 5, затем 8. Новый вызов make_total() создаёт новую total, и новое замыкание начинает с нуля.
Здесь картинка с рюкзаком нуждается в оговорке. В настоящем рюкзаке лежали бы копии, а замыкание держит сами переменные внешнего вызова. Поэтому nonlocal меняет именно их, и следующий вызов видит новое значение.
Посмотрим на это вживую, на двух счётчиках. Нажмите «Следующий шаг» и следите за строкой «захвачена»: у a и b своя count, и она живёт после возврата из make_counter.
1def make_counter():2 count = 03 4 def tick():5 nonlocal count6 count += 17 return count8 return tick9 10 11a = make_counter()12b = make_counter()13print(a(), a(), b())
Программа вызывает make_counter и ждёт, что та вернёт.
Снаружи добраться до count нельзя: её меняет только tick при вызове через a или b.
Частая ошибка
Во вложенной функции забыли nonlocal:
def make_total():
total = 0
def add(x):
total += x # ✗
return total
return add
add = make_total()
print(add(5))UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueИз-за += имя total стало локальным во всей add, а значения у этой локальной total нет. Ошибка возникает при вызове add(5), а не при make_total(). Добавьте nonlocal total первой строкой тела add.
Главное
- Замыкание — вложенная функция, которая использует переменные внешней функции.
- Эти переменные живут после возврата из внешней функции, пока живёт замыкание.
- Каждый вызов внешней функции создаёт новые переменные, поэтому у каждого счётчика своё состояние.
- Чтобы присвоить переменной внешней функции новое значение, пишут
nonlocalи её имя.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.