Замыкания в Python

В этой статье вы узнаете, что такое замыкания, зачем они нужны и как их использовать в Python.

Python позволяет определять функции внутри других функций. «Внутренняя» функция называется вложенной. Вот пример:

def say():
    greeting = "Привет"

    def display():
        print(greeting)

    display()

В этом примере мы объявили функцию display() внутри функции say(). Функция display()вложенная.

При этом в функции display() используется переменная greeting, которая инициализирована вне этой функции, то есть из нелокальной области видимости. 

В Python такие переменные как greeting называются свободными переменными.

На самом деле функция display() состоит из двух частей:

  • Сама функция display().
  • Переменная greeting, в которой содержится значение "Привет".

Так вот все вместе это называется замыканием (closure).

Замыкание (closure) — это вложенная функция, которая ссылается на одну или более переменных из объемлющей (enclosing) области видимости.

Можно вернуть функцию из функции

В Python функция может возвращать другую функцию. Например, так: 

def say():
    greeting = "Привет"

    def display():
        print(greeting)

    return display    

В этом примере функция say() возвращает функцию display(), а не выполняет ее.

Когда say() возвращает display(), на самом деле возвращается замыкание:

Следующая инструкция присваивает возвращаемое значение функции say() переменной fn. Поскольку fn — это функция, ее можно вызвать:

fn = say()
fn() # Вывод: Привет

Функция say() выполняется и возвращает функцию fn(). Когда выполняется fn(), функция say() уже завершила выполнение. 

Это значит, что область видимости функции say() уже не существует к тому моменту, когда выполняется fn().

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

Однако fn() все равно как-то выводит значение переменной greeting, как вы видите в примере выше.

Разберемся, как это работает и почему так происходит.

Ячейки и переменные с несколькими областями видимости

Переменная greeting «разделяется» между двумя областями видимости:

  • Функции say().
  • Замыкания.

То есть greeting находится одновременно в двух областях видимости. Тем не менее, она всегда ссылается на один и тот же строковый объект "Привет".

Для этого Python создает промежуточный объект — ячейку (cell).

Узнать адрес ячейки в памяти можно через свойство __closure__, как показано ниже: 

print(fn.__closure__)

Вывод

(<cell at 0x0000017184915C40: str object at 0x0000017186A829B0>,)

__closure__ возвращает кортеж ячеек.

В этом примере адрес ячейки в памяти — 0x0000017184915C40. Она ссылается на строковый объект по адресу 0x0000017186A829B0.

Если вы отобразите адрес памяти строкового объекта в функции say() и замыкании, то увидите, что они ссылаются на один и тот же объект в памяти:

def say():
    greeting = "Привет"
    print(hex(id(greeting)))

    def display():
        print(hex(id(greeting)))
        print(greeting)

    return display


fn = say()
fn()

Вывод

0x17186a829b0
0x17186a829b0

Когда вы обращаетесь к значению переменной greeting, Python технически дважды «прыгает» в память, чтобы получить значение строки.

Это объясняет, почему когда область видимости функции say() уже не существовала, вы все равно могли обратиться к строковому объекту, на который ссылается переменная greeting.

На основе этого механизма можно определить замыкание по-другому:

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

Чтобы узнать, какие свободные переменные содержатся в замыкание, можно использовать __code__.co_freevars. Например:

def say():

    greeting = "Привет"

    def display():
        print(greeting)

    return display


fn = say()
print(fn.__code__.co_freevars)  # Вывод: ('greeting',)

В этом примере fn.__code__.co_freevars вернет переменную greeting — единственную свободную переменную в замыкании fn.

Когда Python сам создает замыкания

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

1. Сначала определим функцию multiplier(), которая возвращает замыкание:

def multiplier(x):
    def multiply(y):
        return x * y
    return multiply

Функция multiplier() возвращает произведение двух аргументов. Но возвращается не само произведение, а замыкание. 

2. Теперь вызовем функцию multiplier() три раза:

m1 = multiplier(1)
m2 = multiplier(2)
m3 = multiplier(3)

Вызовы функция создадут три замыкания. Каждая функция умножает число на 1, 2, и 3 соответсвенно.

3. А теперь выполним функции замыканий:

print(m1(10))
print(m2(10))
print(m3(10))

Вывод

10
20
30

Как вы видите, у m1, m2 и m3 разные инстансы замыкания.

Замыкания и цикл for

Предположим, что нам нужно создать все три вышеуказанные замыкания одновременно. Это можно сделать, например, так:

multipliers = []
for x in range(1, 4):
    multipliers.append(lambda y: x * y)

m1, m2, m3 = multipliers

print(m1(10))
print(m2(10))
print(m3(10))

Что мы здесь делаем

  1. Объявляем список, в котором будем хранить замыкания.
  2. Используем лямбда-выражение, чтобы создать замыкание, и добавляем его в список на каждой итерации. 
  3. «Распаковываем» замыкания из списка и присваиваем их переменным m1, m2 и m3 соответственно.
  4. Передаем значение 10 в каждое замыкание и выполняем их.

Вывод

30
30
30

Работает не так, как мы ожидали. Почему? 

В цикле for x меняет значения от 1 до 3. Когда цикл завершается, значение x — 3.

Каждый элемент списка — соответсвующее замыкание lambda y: x*y.

Python использует значение x для вычислений каждый раз, когда вы вызываете m1(10), m2(10) и m3(10). И поскольку в момент выполнения замыкания x всегда 3, результат всех замыканий одинаковый — 30.

Чтобы исправить эту проблему, нужно объяснить Python, что значение x для вычислений нужно использовать в цикле.

def multiplier(x):
    def multiply(y):
        return x * y
    return multiply


multipliers = []
for x in range(1, 4):
    multipliers.append(multiplier(x))

m1, m2, m3 = multipliers

print(m1(10))
print(m2(10))
print(m3(10))
Проверьте себя
1. Что такое замыкание (closure) в Python?
AВложенная функция, которая ссылается на переменные из объемлющей области видимости
BКласс, инкапсулирующий данные и методы
CФункция, которая принимает другую функцию в качестве аргумента
DФункция без возвращаемого значения
2. Функция say() вернула функцию fn = say(). После этого функция say() завершила выполнение. Что произойдёт при вызове fn()?
Afn() выведет значение greeting, потому что замыкание сохраняет ссылку на переменную
BВозникнет NameError, так как переменная greeting уже уничтожена
Cfn() вернёт None
DPython повторно вызовет функцию say()
3. Что вернёт атрибут __closure__ функции-замыкания?
AИмена всех свободных переменных в виде строк
BNone, если замыкание не имеет свободных переменных
CКортеж объектов-ячеек, хранящих свободные переменные
DСловарь {имя_переменной: значение}

Закрепите практикой

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

py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →