Замыкания в Python

Поговорим про замыкания (closures) в Python, основные вопросы, на которые мы постараемся ответить, это: Что такое замыкания? Как их использовать? И обсудим свойство замыкания – средство для построения иерархических данных.

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

Для начала обратимся к википедии: “замыкание (closure) в программировании — это функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся ее параметрами.” Перед тем как перейти к рассмотрению примеров реализации замыканий на Python, для начал вспомним тему “область видимости переменных”. Обычно, по области видимости, переменные делят на глобальные и локальные. Глобальные существует в течении всего времени выполнения программы, а локальные создаются внутри методов, функций и прочих блоках кода, при этом, после выхода из такого блока переменная удаляется из памяти.

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

  • Local

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

Пример.

>>> def add_two(a):
        x = 2
        return a + x

>>> add_two(3)
5

>>> print(x)
Traceback (most recent call last):
  File "<pyshell#5>", line 1, in <module>
    print(x)
NameError: name 'x' is not defined

В данной программе объявлена функция add_two(), которая прибавляет двойку к переданному ей числу и возвращает полученный результат. Внутри этой функции используется переменная x, доступ к которой снаружи невозможен. К тому же, эта переменная удаляется из памяти каждый раз (во всяком случае, должна удаляться), когда завершается add_two().

  • Enclosing

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

Пример.

>>> def add_four(a):
        x = 2
        def add_some():
            print("x = " + str(x))
            return a + x
        return add_some()

>>> add_four(5)
x = 2
7

В данном случае переменная x имеет область видимости enclosing для функции add_some().

  • Global

Переменные области видимости global – это глобальные переменные уровня модуля (модуль – это файл с расширением .py).

Пример.

>>> x = 4
>>> def fun():
        print(x+3)

>>> fun()
7

В приведенном выше коде переменная x – это global переменная. Доступ к ней можно получить из любой функции объявленной в данном модуле. Но если мы этот модуль импортируем в каком-то другом модуле, то x для него уже не будет переменной уровня global.

  • Built-in

Уровень Python интерпретатора. В рамках этой области видимости находятся функции open, len и т.п., также туда входят исключения. Эти сущности доступны в любом модуле Python и не требуют предварительного импорта. Built-in – это максимально широкая область видимости.

Как уже было сказано выше, каждый раз, когда мы вызываем функцию, у нее создаются локальные переменные (если они у нее есть), а после завершения – уничтожаются, при очередном вызове эта процедура повторяется. Можно ли сделать так, чтобы после завершения работы функции, часть локальных переменных не уничтожалась, а сохраняла свои значение до следующего запуска? Да, это можно сделать!

Локальная переменная не будет уничтожена, если на нее где-то останется “живая” ссылка, после завершения работы функции. Эту ссылку может сохранять вложенная функция. Функции построенные по такому принципу могут использоваться для построения специализированных функций, т.е. являются фабриками функций. Далее будет рассмотрен вопрос создания и использования замыканий в Python, которые как раз и использую эту идею.

Как использовать замыкания в Python?

Для начала разберем следующий пример.

>>> def mul(a, b):
        return a * b

>>> mul(3, 4)
12

Функция mul() умножает два числа и возвращает полученный результат. Если мы ходим на базе нее решить задачу: “умножить число на пять”, то в самом простом случае, можно вызывать mul(), передавая в качестве первого аргумента пятерку.

>>> mul(5, 2)
10

>>> mul(5, 7)
35

Это неудобно. На самом деле мы можем создать новую функцию, которая будет вызывать mul(), с пятеркой и ещё одним числом, которое она будет получать в качестве своего единственного аргумента.

>>> def mul5(a):
        return mul(5, a)

>>> mul5(2)
10

>>> mul5(7)
35

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

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

Вычислим выражение “5 * 2 = ?” с помощью этой функции.

>>> mul(5)(2)
  10

Создадим функцию – аналог mul5().

>>> new_mul5 = mul(5)

>>> new_mul5
<function mul.<locals>.helper at 0x000001A7548C1158>

>>> new_mul5(2)
10

>>> new_mul5(7)
35

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

Рассмотрим ещё один пример.

>>> def fun1(a):
        x = a * 3
        def fun2(b):
            nonlocal x
            return b + x
        return fun2

>>> test_fun = fun1(4)

>>> test_fun(7)
19

В функции fun1() объявлена локальная переменная x, значение которой определяется аргументом a. В функции fun2() используются эта же переменная x, nonlocal указывает на то, что эта переменная не является локальной, следовательно, ее значение будет взято из ближайшей области видимости, в которой существует переменная с таким же именем. В нашем случае – это область enclosing, в которой этой переменной x присваивается значение a * 3Также как и в предыдущем случае, на переменную x после вызова fun1(4), сохраняется ссылка, поэтому она не уничтожается.

Свойство замыкания – средство для построения иерархических данных

Сразу хочу сказать, что “свойство замыкания” – это не то замыкание, которое мы разобрали выше. Начнем разбор данного термина с математической точки зрения, а точнее с алгебраической. Предметом алгебры является изучение алгебраических структур – множеств с определенными на них операциями. Под множеством обычно понимается совокупность определенных объектов. Наиболее простым примером числового множества, является множество натуральных чисел. Оно содержит следующие числа: 1, 2, 3, … и т.д. до бесконечности. Иногда, к этому множеству относят число ноль, но мы не будем этого делать. Над элементами этого множества можно производить различные операции, например сложение:

1 + 2 = 3

Какие бы натуральные числа мы не складывали, всегда будем получать натуральное число. С умножением точно также. Но с вычитанием и делением это условие не выполняется.

2 – 5 = -3

Среди натуральных чисел нет числа -3, для того, чтобы можно было использовать вычитание без ограничений, нам необходимо расширить множество натуральных чисел до множества целых чисел:

-∞, …, -2, -1, 0, 1, 2, …, ∞.

Таким образом, можно сказать, что множество натуральных чисел замкнуто относительно операции сложения – какие бы натуральные числа мы не складывали, получим натуральное число, но это множество не замкнуто относительно операции вычитания.

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

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

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

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

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

>>> a = tpl(1, 2)
>>> a
(1, 2)

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

>>> b = tpl(3, a)
>>> b
(3, (1, 2))

>>> c = tpl(a, b)
>>> c
((1, 2), (3, (1, 2)))

Таким образом, в нашем примере кортежи оказались замкнуты относительно операции объединения tpl. Вспомните аналогию с натуральными числами, замкнутыми относительно сложения.

Полезные ссылки!

Уроки по языку программирования Python

Помощники цикла for в Python

О рекурсии и итерации

Замыкания в Python: 9 комментариев

  1. Александр Сунгуров

    Спасибо за подробное разъяснение !

  2. даня

    >>> def fun1(a):
    x = a * 3
    def fun2(b):
    nonlocal x
    return b + x
    return fun2

    >>> test_fun = fun1(4)

    >>> test_fun(7)
    19

    Зачем мы в данном примере объявляем переменную “х” как nonlocal?
    ведь в fun2 мы ее не изменяем.

    1. Вадим

      Я тоже думаю, что незачем. Мы ее из вложенной функции и так видим.

    2. Аноним

      Потому что мы все равно обращаемся к этой переменной. Иначе будет ошибка.

  3. Евгений

    Несколько дней замыкания понять нп мог, и тут как понял, благодаря вашей статье

Добавить комментарий

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