Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Замыкания Python: Функции

Помните функцию inner, которую мы создавали в первом уроке по ФВП? Так вот, эта функция была замыканием! Замыкание (closure) — это такая функция, которая ссылается на локальные переменные (использует их в своём теле) в области видимости, в которой она была создана. Этим замыкание отличается от обычной функции, которая может использовать только свои аргументы и глобальные переменные. Рассмотрим пример, демонстрирующий замыкание, и уже на нём разберём что и чем является:

G = 10

def make_closure():
    a = 1
    b = 2
    def inner(x):
        return x + G + a
    return inner

В этом примере inner — замыкание. Переменная b не используется в теле inner и замыканием запомнена не будет. А вот переменная a, напротив, участвует в формировании результата вызова inner и поэтому её значение будет запомнено. А вот G, это глобальная переменная (если принять факт, что указанный код находится на верхнем уровне модуля, т.е. не вложен ни в какие другие блоки).

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

Момент запоминания значений переменных

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

def make_closure():
    y = 1
    def inner(x):
        return x + y
    y = 42
    return inner

make_closure()(100)
# 142

Здесь inner получает в качестве запомненного значения 42, пусть даже присваивание этого значения переменной y происходит и после объявления функции! Ещё "забавнее" выглядит замыкание переменной цикла:

printers = []
for i in range(10):
    def printer():
        print(i)
    printers.append(printer)

printers[0]()
# 9
printers[5]()
# 9
printers[9]()
# 9
i = 42
printers[0]()
# 42

Казалось бы, мы создали десяток функций, каждая из которых должна печатать своё число, но все функции печатают последнее значение переменной цикла! Здесь тоже фактическое запоминание происходит при выходе из области видимости в которой определена переменная i, вот только эта область видимости на момент вызова замыканий ещё не завершилась (в этом REPL-примере она завершится только при выходе из REPL)! Поэтому после выхода из цикла все замыкания выводят 9, а после изменения значения переменной i выводимое значение также меняется!

Борем замыкания!

Как же запоминать то, что нужно и когда нужно? Как же нам починить пример с циклом, чтобы каждая функция печатала своё значение и не реагировала на дальнейшие изменения переменной цикла? Отвечаю: нужно замкнуть переменную в области видимости, которая завершится сразу же после создания замыкания! Этого можно добиться, завернув создание функции в… другую функцию! Вот код:

printers = []
for i in range(10):
    def make_printer(arg):
        def printer():
            print(arg)
        return printer
    p = make_printer(i)
    printers.append(p)

printers[0]()
# 0
printers[5]()
# 5

Результат положительный! Но как же этот код работает? Заметьте, в этот раз printer замыкает значение переменной arg, а она принадлежит функции make_printer и видна только пока выполняется тело функции. А ведь это именно то, что нам нужно: когда происходит выход из тела make_printer, возвращаемое замыкание получает-таки своё значение. А раз функция make_printer вызывается с разными аргументами, то и замыкания получают разные значения!

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
с нуля
Разработка веб-приложений на Django
2 февраля 10 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»