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

В этой статье мы рассмотрим замыкания (closures) в Python: как их определять и когда их стоит использовать.

Нелокальная переменная во вложенной функции

Прежде чем перейти к тому, что такое замыкание, мы должны сначала понять, что такое вложенная функция и нелокальная (nonlocal) переменная.

Функция, определенная внутри другой функции, называется вложенной функцией. Вложенные функции могут получать доступ к переменным из локальной области видимости объемлющих функций (enclosing scope).

В Python нелокальные переменные по умолчанию доступны только для чтения. Если нам необходимо их модифицировать, то мы должны объявить их явно как нелокальные (используя ключевое слово nonlocal).

Ниже приведен пример вложенной функции, обращающейся к нелокальной переменной.

def print_msg(msg):
    # объемлющая функция

    def printer():
        # вложенная функция
        print(msg)

    printer()

# Output: Hello
print_msg("Hello")

Мы видим, что вложенная функция printer() смогла получить доступ к нелокальной переменной msg объемлющей функции print_msg(msg).

Определение замыкания

Что произойдет в приведенном выше примере, если последняя строка функции print_msg() вернет функцию printer() вместо ее вызова? Определим данную функцию следующим образом:

def print_msg(msg):
    # объемлющая функция

    def printer():
        # вложенная функция
        print(msg)

    return printer  # возвращаем вложенную функцию


# теперь попробуем вызвать эту функцию
# Output: Hello
another = print_msg("Hello")
another()

Это необычно.

Функция print_msg() вызывалась со строкой «Hello», а возвращаемая функция была присвоена переменной another. При вызове another() сообщение все еще сохранялось в памяти, хотя мы уже закончили выполнение функции print_msg().

Этот метод, с помощью которого некоторые данные (в данном случае строка «Hello») прикрепляются к некоторому коду, в Python называется замыканием.

Ссылка на переменную объемлющей функции действительна, даже когда объемлющая функция закончила работу, и переменная вышла из области видимости или сама функция удаляется из текущего пространства имен.

Попробуйте выполнить следующее, чтобы увидеть результат:

>>> del print_msg
>>> another()
Hello
>>> print_msg("Hello")
Traceback (most recent call last):
...
NameError: name 'print_msg' is not defined

Здесь возвращаемая функция все еще работает, даже если исходная функция была удалена.

Когда мы имеем дело с замыканием?

Как видно из приведенного выше примера, мы имеем дело с  замыканием в Python, когда вложенная функция ссылается на значение из локальной области видимости объемлющей функции.

Критерии, которые должны быть выполнены для создания замыкания в Python, изложены в следующих пунктах:

  • У нас должна быть вложенная функция (функция внутри функции).
  • Вложенная функция должна ссылаться на значение, определенное в объемлющей функции.
  • Объемлющая функция должна возвращать вложенную функцию.

Когда стоит использовать замыкания?

Так для чего же нужны замыкания?

Замыкания позволяют избежать использования глобальных (global) значений и обеспечивают некоторую форму сокрытия данных. Для этого также может использоваться объектно-ориентированный подход.

Если в классе необходимо реализовать небольшое количество методов (в большинстве случаев один метод), замыкания могут обеспечить альтернативное и более элегантное решение. Но когда количество атрибутов и методов становится больше, лучше реализовать класс.

Вот простой пример, где замыкание может быть более предпочтительным, чем определение класса и создание объектов. Но выбор остается за вами.

def make_multiplier_of(n):
    def multiplier(x):
        return x * n
    return multiplier


times3 = make_multiplier_of(3)

times5 = make_multiplier_of(5)

# Output: 27
print(times3(9))

# Output: 15
print(times5(3))

# Output: 30
print(times5(times3(2)))

Декораторы в Python также широко используют замыкания.

В заключение следует отметить, что можно найти значения, включенные в функцию замыкания.

Все объекты функций имеют атрибут __closure__, который возвращает кортеж объектов cell, если это функция замыкания. Ссылаясь на приведенный выше пример, мы знаем, что times3 и times5 являются замыканиями.

>>> make_multiplier_of.__closure__
>>> times3.__closure__
(<cell at 0x0000000002D155B8: int object at 0x000000001E39B6E0>,)

Объект cell имеет атрибут cell_contents, который хранит значение.

>>> times3.__closure__[0].cell_contents
3
>>> times5.__closure__[0].cell_contents
5

Больше примеров замыканий вы можете найти по ссылке.