Сообщить об ошибке.

Что такое замыкания в функциях Python

Содержание:


Хранение локального состояния функцией.

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

Функция ниже принимает аргумент число n и текст txt, чтобы сделать возвращаемую функцию вызываемой:

def talk(n, name):
    def hello():
        return f'Привет {name}.'
    def goodbye():
        return f'Пока {name}.'
    if n > 0:
        return hello
    else:
        return goodbye

>>> talk(1, 'Андрей')()
# 'Привет Андрей.'

Внимательно посмотрите на внутренние функции hello() и goodbye(). Обратите внимание, что у них больше нет параметра name. Но каким-то образом они могут получить доступ к параметру name, который теперь передается в качестве аргумента родительской функции. Фактически, внутренние функции запоминают значение этого аргумента.

Функции, которые делают это, называются лексическими замыканиями или просто замыканиями. Замыкание запоминает значения из своей заключительной лексической области, даже если поток программы больше не находится в этой области.

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

def mul(a):
    def helper(b):
        return a * b
    return helper

>>> mul5 = mul(5)
>>> mul5(2)
# 10

>>> mul5(7)
# 35

В этом примере mul() служит функцией фабрикой для создания и настройки функции helper().

Вызывая mul5(2), мы фактически обращаемся к функции helper(), которая находится внутри mul(). Переменная a, является локальной для mul(), и имеет область enclosing scope (охватывающая область) в helper(). Несмотря на то, что mul() завершила свою работу, переменная a не уничтожается, т. к. на нее сохраняется ссылка во внутренней функции, которая возвращается в качестве результата.

Еще пример:

def maker(a):
    x = a * 5
    def add(b):
        nonlocal x
        return b + x
    return add

>>> test = maker(6)
>>> test(10)
# 40

В функции maker() объявлена локальная переменная x, значение которой определяется аргументом a. В функции add() используются эта же переменная x, а nonlocal указывает на то, что эта переменная не является локальной, следовательно, ее значение будет взято из ближайшей области видимости, в которой существует переменная с таким же именем. В нашем случае, это область enclosing scope (охватывающая область), в которой этой переменной x присваивается значение a * 5. Также как и в предыдущем случае, на переменную x после вызова make(6), сохраняется ссылка, поэтому она не уничтожается.

Замыкания и позднее связывание переменных в Python.

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

# функция замыкание
def create_multipliers():
    """ Функция возвращает список созданных в цикле объектов 
    функции `multiplier`, по которому потом будем итерироваться.
    """
    # пустой список, в который будут 
    # добавляться объекты `multiplier` 
    # при каждой итерации
    lst = []
    for i in range(5):
        def multiplier(x):
            return i * x
        # добавляем объект функции `multiplier` в список `lst`, 
        # при этом она не вызывается, т.е. еще не выполнилась
        lst.append(multiplier)
    # возвращаем список объектов `multipliers`
    return lst

В представленном замыкании вроде все нормально, переменной i в цикле, в локальной области функции create_multipliers() назначаются числа от 0 до 4. А внутренняя функция, помня эти значения должна вернуть i * x, где переменную x передадим при вызове внутренней функции как замыкания.

# помним, что `create_multipliers()` возвращает
# список объектов `multiplier`. В этом цикле они
# вызываются как `mp()` c аргументом `x=2`
>>> for mp in create_multipliers():
...     print(mp(2), end=', ')

# Скорее всего можно ожидать следующих результатов
# 0, 2, 4, 6, 8

# Но на самом деле получаем:
# 8, 8, 8, 8, 8

Такое поведение происходит из-за позднего связывания в Python, согласно которому значения переменных, используемых в замыканиях, просматриваются (извлекаются из локальной области функции) во время вызова внутренней функции. Таким образом, в приведенном выше коде всякий раз, когда вызывается какая-либо из возвращаемых функций, значение i ищется в окружающей области во время ее вызова, а к тому времени цикл for i in range(5) завершен, следовательно переменной i уже назначено его окончательное значение 4. НЕ ПОПАДАЙТЕСЬ!

Решение этой распространенной проблемы Python заключается в передаче вложенной функции multiplier() переменной i, как аргумента по умолчанию:

def create_multipliers():
    lst = []
    for i in range(5):
        def multiplier(x, i=i): # решение
            return i * x
        lst.append(multiplier)
    return lst

>>> for mp in create_multipliers():
...     print(mp(2), end=', ')

# 0, 2, 4, 6, 8

Замыкания и построение иерархических данных.

Вот как определяется “свойство замыкания” в книге “Структура и интерпретация компьютерных программ” Айбельсона Х., Сассмана Д.Д.:

“В общем случае, операция комбинирования объектов данных обладает свойством замыкания в том случае, если результаты соединения объектов с помощью этой операции сами могут соединяться этой же операцией”.

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

Создадим функцию union(), которая на вход принимает два аргумента и возвращает кортеж. Эта функция реализует операцию объединения элементов в кортеж.

>>> union = lambda a, b: (a, b)

Передав в качестве аргументов числа, получим простой кортеж.

>>> x = union(7, 9)
>>> x
# (7, 9)

Эту операцию можно производить не только над числами, но и над сущностями, ей же порожденными.

>>> y = union(5, x)
>>> y
# (5, (7, 9))

>>> union(x, y)
# ((7, 9), (5, (7, 9)))

Таким образом, кортежи оказались замкнуты относительно операции объединения union().