Замыкания в 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__)
Вывод
__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))
Что мы здесь делаем
- Объявляем список, в котором будем хранить замыкания.
- Используем лямбда-выражение, чтобы создать замыкание, и добавляем его в список на каждой итерации.
- «Распоковываем» замыкания из списка и присваиваем их переменным
m1
,m2
иm3
соответсвенно. - Передаем значения 10, 20 и 30 в каждое замыкание и выполняем их.
Вывод
30
30
30
Работает не так, как мы ожидали. Почему?
В цикле for x
меняет значения от 1 до 3. Когда цикл завершается, значение x
— 3.
Каждый элемент списка — соответсвующее замыкание lambda y: x*y
.
Python использует значение x
для вычислений каждый раз, когда вы вызываются m1(1)
, 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))