Замыкания в 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, 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))
codechick

СodeСhick.io - простой и эффективный способ изучения программирования.

2024 ©