Замыкания (closure) в Python на примерах

В этом уроке вы узнаете о замыканиях (closure) в Python и их практическом применении.

Содержание

Что такое замыкание в Python?

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

def say():
    greeting = 'Hello'

    def display():
        print(greeting)

    display()

В этом примере мы определяем функцию отображения внутри функции Say. Функция отображения называется вложенной функцией.

Внутри функции display вы получаете доступ к переменной greeting из ее нелокальной области видимости. Python называет переменную greeting свободной переменной.

Когда вы смотрите на функцию display, вы на самом деле смотрите на:

  • Непосредственно саму функцию display.
  • И свободную переменную greeting со значением «Hello».

Таким образом, комбинация функции display и переменной greeting называется замыканием:

Комбинация функции display и переменной greeting в Python

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

Возврат внутренней функции

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

def say():
    greeting = 'Hello'

    def display():
        print(greeting)

    return display    

В этом примере функция Say возвращает функцию отображения вместо ее выполнения. Кроме того, когда функция Say возвращает функцию display, она фактически возвращает замыкание:

Возврат функции display в Python

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

fn = say()
fn()

Выход:

Hello

Функция Say выполняет и возвращает функцию. Когда функция fn выполняется, функция Say уже завершается. Другими словами, область действия функции Say исчезла во время выполнения функции fn.

Поскольку переменная приветствия принадлежит области действия функции Say, ее также следует уничтожить вместе с областью действия функции.

Однако вы по-прежнему видите, что fn отображает значение переменной message.

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

Значение переменной greeting распределяется между двумя областями:

  • Функция say
  • Замыкание

Метка greeting имеет две разные области действия. Однако они всегда ссылаются на один и тот же строковый объект со значением «Hello».

Для этого Python создает промежуточный объект, называемый ячейкой:

Создание ячейки в Python

Чтобы найти адрес памяти объекта ячейки, вы можете использовать свойство __closure__ следующим образом:

print(fn.__closure__)

Выход:

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

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

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

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

def say():
    greeting = 'Hello'
    print(hex(id(greeting)))

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

    return display


fn = say()
fn()

Выход:

0x17186a829b0
0x17186a829b0

Когда вы получаете доступ к значению переменной greeting, Python технически выполняет «двойной переход», чтобы получить строковое значение. Это объясняет, почему, даже если функция Say() вышла за пределы области действия, вы все равно можете получить доступ к строковому объекту, на который ссылается переменная приветствия.

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

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

def say():

    greeting = 'Hello'

    def display():
        print(greeting)

    return display


fn = say()
print(fn.__code__.co_freevars)

Выход:

('greeting',)

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

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

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

  • Сначала определите функцию multiplier, которая возвращает замыкание:
def multiplier(x):
    def multiply(y):
        return x * y
    return multiply

Функция multiplier возвращает произведение двух аргументов. Однако вместо этого она использует замыкание.

  • Во-вторых, вызовите функцию multiplier три раза:
m1 = multiplier(1)
m2 = multiplier(2)
m3 = multiplier(3)

Эти вызовы функций создают три замыкания. Каждая функция умножает число на 1, 2 и 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

Это работает не так, как вы ожидали. Но почему? В цикле значение x начинается с 1 до 3. После цикла его значение равно 3.

Каждый элемент списка представляет собой следующее замыкание:

lambda y: x*y

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

Чтобы это исправить, вам нужно указать 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))

 

Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *