Перейти к содержимому
Теория · Функции как значения · урок 5 из 10

Замыкания

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

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

Нужны функции, которые умножают число на 2, на 3 и на 10. Три отдельных def отличаются одним числом. Вместо них можно написать функцию, которая создаёт такую функцию по числу и возвращает её:

Python
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

Давайте разберём пример по строкам:

  1. make_multiplier(2) создаёт новую функцию multiply и возвращает её. Она попадает в double.
  2. make_multiplier(3) создаёт ещё одну multiply, она попадает в triple.
  3. double(5) умножает 5 на 2, а triple(5) — на 3, поэтому печатается 10 15.
  4. 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 для глобальной переменной:

Python
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))  # 10

total = 0 выполняется один раз, при вызове make_total, а не при каждом вызове add. Поэтому add копит сумму: 5, затем 8. Новый вызов make_total() создаёт новую total, и новое замыкание начинает с нуля.

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

Посмотрим на это вживую, на двух счётчиках. Нажмите «Следующий шаг» и следите за строкой «захвачена»: у a и b своя count, и она живёт после возврата из make_counter.

ВживуюДва счётчикашаг 1 из 11 · рамок: 1

Программа вызывает make_counter и ждёт, что та вернёт.

Строка «захвачена» — переменные, которые держат замыкания

Снаружи добраться до count нельзя: её меняет только tick при вызове через a или b.

Частая ошибка

Во вложенной функции забыли nonlocal:

Python
def make_total():
    total = 0

    def add(x):
        total += x  # ✗
        return total
    return add


add = make_total()
print(add(5))
TEXT
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 и подпишись на канал — откроются все уроки.

Войти другим способом