Если видео недоступно для просмотра, попробуйте выключить блокировщик рекламы.

Замыкания

Помните функцию 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.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →