Замыкания в Python

Замыкание в Python — это вложенная функция, которая позволяет обращаться к переменным внешней функции даже после её завершения.

Прежде чем изучать замыкания, повторим концепцию вложенных функций в Python.


Вложенные функции в Python

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

def greet(name):
    # внутренняя функция
    def display_name():
        print("Hi", name)

    # вызов внутренней функции
    display_name()

# вызов внешней функции
greet("John")

# Вывод: Hi John

В приведённом примере мы определили функцию display_name() внутри функции greet().

Здесь display_name() — вложенная функция. Вложенная функция работает аналогично обычной функции. Она выполняется, когда display_name() вызывается внутри функции greet().


Замыкания в Python

Как мы уже обсуждали, замыкание — это вложенная функция, которая помогает обращаться к переменным внешней функции даже после её завершения. Например,

def greet():
    # переменная, определённая вне внутренней функции
    name = "John"

    # возврат вложенной анонимной функции
    return lambda: "Hi " + name

# вызов внешней функции
message = greet()

# вызов внутренней функции
print(message())

# Вывод: Hi John

В приведённом примере мы создали функцию greet(), возвращающую вложенную анонимную функцию.

Здесь, когда мы вызываем внешнюю функцию:

message = greet()

Возвращённая функция теперь присваивается переменной message.

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

print(message())

мы можем обратиться к переменной name внешней функции.

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

Рассмотрим ещё один пример, чтобы прояснить эту концепцию.


Пример: вывод нечётных чисел с помощью замыкания

def calculate():
    num = 1
    def inner_func():
        nonlocal num
        num += 2
        return num
    return inner_func

# вызов внешней функции
odd = calculate()

# вызов внутренней функции
print(odd())
print(odd())
print(odd())

# повторный вызов внешней функции
odd2 = calculate()
print(odd2())

Вывод

3
5
7
3

В приведённом примере:

odd = calculate()

Этот код выполняет внешнюю функцию calculate() и возвращает замыкание для нечётного числа.

Поэтому мы можем обращаться к переменной num функции calculate() даже после завершения внешней функции.

Снова, когда мы вызываем внешнюю функцию через:

odd2 = calculate()

возвращается новое замыкание. Поэтому при вызове odd2() мы снова получаем 3.


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

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

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

Однако для более крупных случаев с несколькими атрибутами и методами реализация через класс может быть более подходящей.

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

# умножитель на 3
times3 = make_multiplier_of(3)

# умножитель на 5
times5 = make_multiplier_of(5)

# Вывод: 27
print(times3(9))

# Вывод: 15
print(times5(3))

# Вывод: 30
print(times5(times3(2)))

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

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

У всех функциональных объектов есть атрибут __closure__, который возвращает кортеж объектов-ячеек, если это функция-замыкание.

Возвращаясь к примеру выше, мы знаем, что times3 и times5 — функции-замыкания.