5 важных аспектов замыканий в Python

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

1. Внутренние и внешние функции

Как я уже отмечал, замыкания есть во многих языках программирования, а в Википедии им дано следующее определение:

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

Это определение для многих покажется слишком техническим и не относится конкретно к Python. Чтобы помочь вам понять этот принцип, я выражу его более просто: замыкание  —  это внутренняя функция, создаваемая во внешней функции и использующая ее переменные. Она возвращается внешней функцией в виде ее выходного значения. Не слишком технически? Думаю, слишком. Лучше всего показать на примере. Начнем со следующего фрагмента кода.

Multiplier creator:

def multiplier_creator(n):
    def multiplier(number):
        return number * n

    return multiplier

double_multiplier = multiplier_creator(2)
triple_multiplier = multiplier_creator(3)

В этом коде double multiplier и triple_multiplier представляют два замыкания, так как соответствуют их определению:

  1. multipler является внутренней функцией, созданной внутри multiplier_creator, которая называется внешней функцией, поскольку находится вне multiplier. Внутренняя функция зачастую зовется вложенной, так как, по сути, вкладывается в другую функцию.
  2. Созданная внутренняя функция представляет значение, возвращаемое внешней функцией. Стоит отметить, что multiplier_creator возвращает multiplier непосредственно вместо выходного значения функции multiplier.
  3. multiplier использует переменную n, являющуюся параметром multiplier_creator. Доступ внутренней функции к внешним переменным называется нелокальной привязкой переменных.

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

2. Локальные и нелокальные переменные

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

Что же касается примера выше, то в нем функция multiplier_creator определяет локальную область видимости, а параметр n является локальной переменной. Аналогичным образом внутренняя функция multiplier определяет еще одну внутреннюю область, где локальной переменной выступает параметр number. При этом стоит заметить, что функция multiplier также использует параметр n. Несмотря на то, что n является локальной переменной для области multiplier_creator, для multiplier она нелокальная. Таким образом, в этом примере функция multiplier использует нелокальную переменную, также известную как свободная переменная.

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

Как вы могли заметить, то с позиции внутренней функции мы называем процесс обращения к локальным переменным внешней функции привязкой нелокальных переменных. Использование слова “привязка” в этом случае примечательно. Что конкретно оно значит? Разберем это далее.

3. Привязка нелокальных переменных

Привязка нелокальных переменных в ряде других языков называется захватом нелокальных переменных, что передает характеристику замыканий. Можно просто представить себе этот процесс как “владение” используемыми нелокальными переменными внутренней функцией. Рассмотрим это на примере:

>>> del multiplier_creator
>>> double_multiplier(5)
10
>>> triple_multiplier(5)
15

В этом коде мы удаляем внешнюю функцию multiplier_creator, в результате чего она становится недоступной. Но обратите внимание, что замыкания (т.е. double_multiplier и triple_multiplier) используют параметр n, принадлежащий удаляемой multiplier_creator. В итоге можно подумать, что замыкания перестанут работать. Однако они по-прежнему выдают ожидаемый результат.

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

>>> double_multiplier.__code__.co_freevars
('n',)
>>> double_multiplier.__closure__[0].cell_contents
2
>>> triple_multiplier.__code__.co_freevars
('n',)
>>> triple_multiplier.__closure__[0].cell_contents
3

__code__.co_freevars позволяет проверить имя привязки нелокальной переменной для замыканий, а __closure__[0].cell_contents уточняет значение привязанной нелокальной переменной. Вам не обязательно знать подробности этих функций, так как они являются просто внутренними реализациями.

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

4. Ключевое слово Nonlocal и исключение Unboundlocalerror

При создании замыканий программисты иногда получают исключение UnboundLocalError. Разберем на примере:

>>> def running_total_multiplier_creator(n):
...     running_total = 0
...     def multiplier(number):
...         product = number * n
...         running_total += product
...         return running_total
...     return multiplier
... 
>>> running_doubler = running_total_multiplier_creator(2)
>>> running_doubler(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in multiplier
UnboundLocalError: local variable 'running_total' referenced before assignment

На первый взгляд функция running_total_multiplier_creator выглядит подходящей для генерации замыкания. Однако при использовании получившегося замыкания возникает исключение UnvoundLocalError. Что означает это исключение? Если прочесть сообщение Traceback, то станет ясно, что проблема заключается в коде running_total += product. Почему он вызывает подобную ошибку? Вот объяснение:

  1. Эта строка кода, по сути, интерпретируется как running_total = running_total + product.
  2. Порядок поиска переменной определяется правилом LEGB, что соответствует последовательности локальная->замыкающая->глобальная->встроенная. При выполнении running_total = xxx во внутренней функции мы регистрируем локальную переменную в локальной области этой функции. Когда Python продолжает выполнять код справа от инструкции присваивания, он снова встречает переменную running_total, в связи с чем начинает искать эту переменную и находит ее в локальной области. Однако он замечает, что ей не было присвоено какое-либо значение, потому что инструкция присваивания не закончила свое выполнение. Именно поэтому сообщение об ошибке гласит: local variable ‘running_total’ referenced before assignment. Когда обращение к переменной происходит до момента присваивания ей значения, это называется ошибкой отсутствия привязки (unbound error).

Если вернуться назад и взглянуть на функцию multiplier, то станет видно, что мы хотели использовать переменную tunning_total, определенную в области running_total_multiplier_creator, с позиции функции multiplier называемую замыкающей областью. Для того, чтобы внутренняя функция поняла, что running_total не задумывалась как локальная переменная, нужно заявить об этом с помощью ключевого слова nonlocal. Вот исправленный вариант:

>>> def running_total_multiplier_creator(n):
...     running_total = 0
...     def multiplier(number):
...         nonlocal running_total
...         product = number * n
...         running_total += product
...         return running_total
...     return multiplier
... 
>>> running_doubler = running_total_multiplier_creator(2)
>>> running_doubler(5)
10

Как видите, мы просто объявили, что running_total является нелокальной переменной, указав Python обходить локальную область при поиске переменной running_total.

5. Почему замыкания?

Мы рассмотрели, чем являются замыкания, но вас может заинтересовать, зачем вообще они нужны? Одно из наиболее распространенных применений состоит в создании функций-декораторов. Несмотря на то, что замыкания вы можете и не знать, я уверен, что о декораторах вы слышали наверняка. В Python декораторы  —  это функции, которые изменяют поведение других функций, не влияя на их алгоритмы.

 Вот пример:

def simple_logger(func):
    def decorated(*args, **kwargs):
        print(f"You're about to call {func}")
        result = func(*args, **kwargs)
        print(f"You just called {func}")
        return result

    return decorated

@simple_logger
def hello_world():
    print("Hello, World!")

Функция simple_logger является декоратором, использование которого подразумевает наличие перед именем символа @ и размещение самого его над декорируемой функцией. При вызове функции hello_world произойдет следующее:

>>> hello_world()
You're about to call <function hello_world at 0x101ce9790>
Hello, World!
You just called <function hello_world at 0x101ce9790>

Суть в том, что декорированная функция по факту является замыканием. В процессе декорации внутренне происходит два этапа:

# Этап 1
def hello_world():
    print("Hello, World!")

# Этап 2
hello_world = simple_logger(hello_world)

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

>>> hello_world.__code__.co_freevars
('func',)
>>> hello_world.__closure__[0].cell_contents
<function hello_world at 0x101ce9790>
  • Декорированная функция hello_world содержит нелокальную переменную func.
  • Привязанное значение является функцией. При этом также важно знать, что функции в Python  —  это просто регулярные объекты, а значит их можно рассматривать как переменные других моделей данных (например, списки и словари).

Заключение

В этой статье мы рассмотрели пять наиболее важных аспектов замыканий. Вот их краткий обзор:

  • Между внутренней и внешней функциями есть отличие. Замыкание  —  это внутренняя функция, созданная во внешней.
  • Замыкание подразумевает привязку нелокальных переменных.
  • У функции есть локальная область видимости. Используя переменные, созданные внутри функции, мы говорим, что используем локальные переменные. Если же задействуются переменные, созданные вне функции, то речь идет о нелокальных. 
  • Ключевое слово nonlocal означает, что переменная должна рассматриваться как нелокальная. Обычно поиск переменных следует правилу LEGB. Тем не менее при использовании ключевого слова nonlocal Python при поиске будет обходить локальную область.
  • Декорирование  —  это процесс создания замыкания, которое замещает объявление исходной функции. Любая декорированная функция внутренне является замыканием.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Yong Cui: 5 Essential Aspects of Python Closures

Предыдущая статья5 лучших бесплатных текстовых редакторов для Windows