Замыкания в 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 в каждое замыкание и выполняем их.
Вывод
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))Закрепите практикой
Задачи с автоматической проверкой — решайте прямо здесь, не уходя из учебника.