Замыкание (Closure)#
Замыкание (closure) — функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле внешней функции (свободные переменные).
Внутренняя функция создается каждый раз во время выполнения внешней. Каждый раз при вызове внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.
Ссылки на переменные внешней функции действительны внутри вложенной функции до тех пор, пока работает вложенная функция, даже если внешняя функция закончила работу, и переменные вышли из области видимости.
Тут замыканием является функция inner. Функция inner использует внутри себя переменную num1 — параметр функции multiply, поэтому переменная num1 будет запомнена, а вот переменная var не используется и запоминатся не будет.
Использование созданной функции выглядит так:
Сначала делается вызов функции multiply с передачей одного аргумента, значение которого запишется в переменную num1:
Переменная mult_by_9 ссылается на внутреннюю функцию inner и при этом внутренняя функция помнит значение num1 = 9 и поэтому все числа будут умножаться на 9:
Еще один пример замыкания с несколькими свободными переменными:
Изменение свободных переменных#
Для получения значения свободной переменной достаточно обратиться к ней, однако, при изменении значений есть нюансы. Если переменная ссылается на изменяемый объект, например, список, изменение содержимого делается стандартным образом без каких-либо проблем. Однако если необходимо, к примеру, добавить 1 к числу, мы получим ошибку:
Если необходимо присвоить свободной переменной другое значение, необходимо явно объявить ее как nonlocal:
Использование nonlocal нужно только если необходимо менять свободную переменную сохраняя измененное значение между вызовами внутренней функции. Для обычного переприсваивания значения ничего делать не нужно.
Изучаем продвинутые возможности Python, часть 2: замыкания, декораторы, модуль functools

В первой части серии публикаций о продвинутых возможностях Python мы познакомились с итераторами, генераторами и модулем itertools. В сегодняшней публикации речь пойдёт о замыканиях, декораторах и модуле functools.
Декораторы
Декоратор — паттерн проектирования, при использовании которого класс или функция изменяет или дополняет функциональность другого класса или функции без использования наследования или прямого изменения исходного кода. В Python декораторы представляют собой функции или любые вызываемые объекты, которые принимают на вход набор необязательных аргументов и функцию или класс и возвращают функцию или класс. Их можно использовать для реализации паттерна проектирования декоратора или для решения других задач. Декораторы классов появились в Python 2.6.
Кстати, если вы не знакомы с замыканиями Python, прежде чем читать дальше ознакомьтесь с дополнением о замыканиях в конце этой статьи. Концепцию декораторов сложно понять, если вы не знакомы с замыканиями.
В Python декораторы применяются к функции или классу с помощью символа @ . В качестве первого примера давайте используем простой декоратор, который регистрирует вызовы функций. В этом примере декоратор принимает формат времени в качестве аргумента и печатает лог перед и после выполнения декорированной функции с временем исполнения. Это может быть кстати, когда вы сравниваете эффективность разных реализаций алгоритма или разных алгоритмов.
Посмотрите на пример использования. Здесь функции add1 и add2 оформлены с помощью logged , а также дан пример вывода. Заметьте, что формат времени хранится в замыкании возвращаемых функций с декоратором. Поэтому понимание замыканий необходимо для понимания декораторов Python.
Также обратите внимание, как имя возвращаемой функции заменяется именем оригинальной функции в случае, если оно используется позже. Python не делает этого по умолчанию.
Если вы достаточно внимательны, то заметите, что мы заботимся, чтобы у возвращаемой функции был правильно указан __name__ , но не заботимся о __doc__ или __module__ . Поэтому если у функции add есть строка документации, она потеряется. Как можно этого избежать? Мы могли бы справиться с проблемой так же, как при обработке __name__ . Но выполнять такие операции с каждым декоратором утомительно. Поэтому в модуле functools есть декоратор wraps , который срабатывает именно в таком сценарии. Использование декоратора внутри другого декоратора может показаться странным. Но если вы думаете о декораторах как о функциях, которые принимают функции в качестве параметров и возвращают функции, всё становится на места. Декоратор wraps используется в следующих примерах вместо ручной обработки __name__ и других подобных атрибутов.
Следующий пример немного сложнее. Давайте напишем декоратор, который кэширует результат вызова функции в течение указанного в секундах времени. Код ожидает, что переданные в функцию аргументы — хэшируемые объекты (hashable objects), потому что мы используем кортеж с аргументами args в качестве первого параметра и замороженный набор элементов в kwargs в качестве второго параметра, который выступает ключом кэша. У каждой функции будет уникальный кэш dict , который хранится в замыкании функции.
Вот как это используется. Мы применяем декоратор к наивному и неэффективному калькулятору чисел Фибоначчи. Декоратор кэша эффективно применяет к коду паттерн мемоизации. Обратите внимание, что в замыкании fib находятся кэш dict , ссылка на исходную функцию fib , значение аргумента logged , а также значение аргумента timeout . dump_closure описывается в конце статьи после раздела о замыканиях.
Декораторы класса
В предыдущем разделе мы рассмотрели декораторы функций и некоторые необычные способы их применения. Теперь давайте рассмотрим декораторы классов. В данном случае декоратор принимает на вход класс (объект с типом type в Python) и возвращает модифицированный класс.
Первый пример — простая математика. Дано частично упорядоченное множество P. Мы определяем Pd как дуальность P, исключительно если P(x,y)⟺Pd(y,x). Другими словами, речь идёт об обратном порядке. Как можно реализовать это с помощью Python? Предположим, класс определяет порядок с помощью методов __lt__ , __le__ и так далее. Тогда мы можем написать декоратор класса, который заменяет каждую функцию её дуальностью.
Вот как это можно применить к str , чтобы создать новый класс rstr , в котором используется обратный лексикографический порядок.
Давайте посмотрим на более сложный пример. Предположим, мы хотим применить декоратор logged из предыдущего примера ко всем методам в классе. Это можно сделать вручную: просто добавить декоратор в каждый метод. Также можно автоматизировать процесс с помощью декоратора класса. Прежде чем сделать это, автор улучшил декоратор logged из предыдущего раздела. Теперь в нём используется атрибут wraps из модуля functools вместо ручной работы с __name__ . Также здесь в возвращаемую функцию добавлен атрибут _logged_decorator . Его значение True , он применяется, чтобы избежать двойного декорирования функции. Это удобно, когда мы применяем декоратор к классам, которые должны наследовать методы от других классов. Наконец, добавлен аргумент name_prefix , который делает возможной кастомизацию сообщений лога.
Теперь можно написать декоратор класса.
Вот как он будет использоваться. Обратите внимание, как здесь обрабатываются переопределённые методы и наследование.
Наш первый пример декораторов класса должен был изменять порядок методов класса. Похожий декоратор, но более полезный, может принимать один из __lt__ , __le__ , __gt__ или __ge__ и __eq__ , и реализовывать остальные для полного упорядочивания класса. Это именно то, что делает декоратор functools.total_ordering . Подробности в документации.
Несколько примеров из Flask
Рассмотрим несколько интересных примеров использования декораторов в Flask.
Представьте, что хотите, чтобы некоторые функции выводили предупреждающие сообщения, если они вызываются при определённых обстоятельствах в режиме отладки. Вместо того, чтобы вручную добавлять код в начало каждой функции, можно использовать декоратор. Это то, что делает декоратор, который можно найти в файле app.py Flask.
Более интересный пример — декоратор Flask route , который определяется в классе Flask . Заметьте, что декоратор может быть методом класса. В этом случае в качестве первого параметра используется self . Полный код смотрите в файле app.py. Обратите внимание, декоратор просто регистрирует декорированную функцию как обработчик URL с помощью вызова функции add_url_rule .
Дополнительное чтение
Много информации о декораторах вы найдёте на официальной вики-странице Python. Также можно посмотреть замечательное видео Дэвида Безли о метапрограммировании в Python 3.
Приложение: замыкания
Замыкание — это комбинация функции и множества ссылок на переменные в области видимости функции. Последнее иногда называют ссылочной средой. Замыкание позволяет выполнять функцию за пределами области видимости. В Python ссылочная среда хранится в виде набора ячеек. Доступ к ним можно получить с помощью атрибутов func_closure или __closure__ . В Python 3 используется только __closure__ .
Важно понимать, что речь идёт просто о ссылках, а не о глубоких копиях объектов. Конечно, неважно, являются ли объекты неизменяемыми, но для изменяемых объектов, например, списков, это важно. Это иллюстрирует пример ниже. Обратите внимание, у функций также есть __globals__ , где хранится глобальное ссылочное окружение, для которого была определена функция. Посмотрите на простой пример:
Ещё один пример, более сложный. Убедитесь, что понимаете, почему код работает именно так.
Наконец, вот пример метода dump_closure , который использовался выше.
Адаптированный перевод статьи A Study of Python’s More Advanced Features Part II: Closures, Decorators and functools by Sahand Saba. Мнение автора оригинальной публикации может не совпадать с мнением администрации «Хекслета».
Вложенные функции, замыкания и декораторы¶
Функция — часть программы, которую можно вызвать из другого места программы.
Все в Python объекты. И даже функции. Это значит, что у функций есть
- атрибуты
- и методы.
От остальных объектов функции отличаются тем, что их можно вызвать*. Объекты, которые можно вызвать, называют Callable -объектами. У них есть метод __call__() .
* С точки зрения синтаксиса еще можно вызывать классы
Как определить функцию¶
Это избыточное определение. Из избыточного здесь использованы:
- строка документирования — docstring ,
- и анотация функции.
На самом деле можно описать эту же функцию компактней.
Функция как объект¶
Как у любого объекта в python, у функции есть:
- идентификатор,
- тип.
В CPython идентификатор — адрес объекта в виртуальной памяти
Все атрибуты и методы функции как объекта можно посмотреть:
Как вызвать функцию¶
Как функции устроены¶
Если заглянуть во внутренности интерпретатора (CPython), то функция описывается следующей струтурой: https://github.com/python/cpython/blob/3.7/Include/funcobject.h
Почитать¶
2. Вложенные функции¶
Вложенная функция — функция, которая определена внутри другой функции.
При работе с вложенными функциями надо учитывать области видимости.
Область видимости в Python — LEGB¶
В Python есть 4 области видимости. Расположены они как показано на рисунке.
Стрелки на рисунке показывают в какой последовательности Python обходит области видимости. Следующий код показывает как распределены области относительно вложенной функции inner.
Зачем нужны вложенные функции?¶
Зачем это может быть нужно? Можно выделить 3 примера:
- чтобы скрыть функцию в глобальной области видимости,
- чтобы вынести «лишний» код из функцию в обёртку,
- чтобы реализовать замыкания (см. следующий раздел).
Пример 1. Чтобы скрыть функцию — инкапсуляция¶
Вложенные функции дают накладные расходы
Пример 2. Чтобы вынести «лишний» код из функции в обёртку¶
3. Замыкания¶
Замыкание — вложенная функция, которая запоминает значения окружения, с которым она была вызвана. Говорят, что функция «замыкается» на значения переменных окружения. По сути это техника параметризированной генерации функций.
Рассмотрим простой пример замыкания
Функции, которые возвращают другие функции, называются «фабриками функций».
Проблема late binding¶
В замыканиях все переменные внутри вложенной функции вычисляются в момент её вызова, а не создания. Это называется позднее связывание (англ. late binding). Из-за него могут возникнуть проблемы, как в примере ниже.
Вместо ожидаемых двух степеней 5-ки получи два раза возведение в последнюю степень из списка. Это происходит из-за того, что в момент вызова p(5) в теле функции переменная i «смотрит» на последнее своё значение, т. е. 2.
Это можно исправить, если создавать копии i при создании замыканий. Это можно сделать с помощью обёртки. При каждом вызове функции параметры указывают на значения аргументов функции. Поэтому замыкания «замыкаются» на текущие значения счётчика.
Другое решение — использовать тот факт, что значения параметров по-умолчанию вычисляются при создании функции.
4. Декораторы¶
Декоратор — «синтаксический сахар» для функции-обёртки вокруг другой функции. Обычно декоратор используют, чтобы добавить новое поведение другой функции без изменения ее тела.
Пример элементарного декоратора¶
Мы хотим обёрнуть функцию возведения в квадрат другой функцией, чтобы добавить новые возможности. Ниже приведен пример фабрики функций, которая возвращает функцию-обёртку. Эта функция-обёртка выполняет новый код и вызывает оборачивемую функцию.
Избегайте рекурсии в Python: вспомните о замыкании

Раньше я был программистом, которому очень нравились рекурсивные функции, просто потому, что это очень круто, с их помощью можно продемонстрировать свои навыки программирования и интеллект. Однако в большинстве случаев рекурсивные функции имеют высокую сложность, поэтому нам следует избегать их использования.
Одно из решений намного лучше – по возможности задействовать динамическое планирование: вероятно, оно – лучший способ решать задачи, которые можно разделить на подзадачи. Одна из моих предыдущих статей демонстрирует мощь динамического планирования.
Здесь я представлю ещё одну технику Python, которую можно использовать как альтернативу рекурсивной функции. Эта техника не превосходит динамическое планирование, но она гораздо проще, с точки зрения мышления. Другими словами, иногда сложно заставить динамическое планирование работать из-за абстракции идей, тогда как проще воспользоваться замыканием.
Что такое замыкание в Python?
Прежде всего позвольте мне на простом примере продемонстрировать, что такое замыкание в Python. Посмотрите на функцию ниже:
Функция outer определяется с функцией inner внутри, а функция outer возвращает функцию inner ; именно она – возвращаемое значение outer .
Здесь вложенная функция – это и есть замыкание. Если проверить возвращаемое значение внешней функции, окажется, что оно является функцией.

Что делает замыкание? Поскольку оно вернуло функцию, мы, конечно, можем запустить её.

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

Следовательно, мы также можем сказать, что в замыкании Python мы определили функцию, которая определяет функции.
Доступ к внешним переменным из внутренней функции
Как тогда мы можем использовать замыкание, чтобы заменить им рекурсию? Не торопитесь. Давайте посмотрим на другую проблему, связанную с доступом к внешним переменным из внутренней функции.
В замыкании выше мы хотим добавить 1 к внешней переменной x во внутренней функции. Это работает неочевидным образом.

По умолчанию вы не сможете получить доступ к внешней переменной из внутренней функции. Однако так же, как мы определяем глобальную переменную в Python, мы можем сообщить внутренней функции замыкания, что переменная не должна рассматриваться как «локальная», это делается с помощью ключевого слова nonlocal .
Теперь предположим, что мы хотим пять раз добавить единицу к переменной x. Можно просто написать цикл for .

Фибоначчи с помощью замыкания
Фибоначчи обычно используется как пример рекурсивных функций, как рекурсивный «Hello, World!». Напомню, о чём речь. Последовательность Фибоначчи – это ряд чисел, каждое следующее число – это сумма двух чисел перед ним. Первые два числа, X₀ и X₁, особенные. Это 0 и 1. Значит, X₂, как упоминалось выше, – это сумма X₀ и X₁. И так далее [сократил].
Рекурсивная функция требует, чтобы мы мыслили в обратном порядке, от «текущей ситуации» к «предыдущей ситуации» и, в конечном счёте, об условии завершения рекурсии. При помощи замыкания можно думать о проблеме более естественным образом. В коде ниже показана реализация Фибоначчи через замыкание:
Мы знаем, что Фибоначчи начинается с двух специальных чисел X₀ = 0 и X₁ = 1, поэтому просто определяем их во внешней функции. Затем внутренняя функция get_next_number просто возвращает сумму двух чисел, полученных от внешней функции. Кроме того, не забудьте обновить X₀ и X₁ с помощью X₁ и X₂. Код можно упростить:
Этот код сначала обновляет две переменные, а затем возвращает вторую, что эквивалентно коду выше. Затем мы можем использовать это замыкание, чтобы вычислить числа Фибоначчи. Например, вот последовательность Фибоначчи до двадцатого.


Сравниваем производительность
А как насчёт производительности? Давайте сравним! Сначала реализуем функцию Фибоначчи рекурсивно:
Функцию можно проверить: вывести 20-е число последовательности Фибоначчи.

Теперь напишем то же самое с замыканием.


2,79 мс против 2,75 мкс. Замыкание в 1000 раз быстрее рекурсии! Интуитивно понятно: причина в том, что все временные значения для каждого уровня рекурсии хранятся в памяти отдельно, тогда как замыкание каждый раз обновляет одни и те же переменные. Кроме того, существует ограничение глубины рекурсии. В случае замыкания, поскольку это в основном цикл for, никаких ограничений нет. Вот пример получения 1000-го числа Фибоначчи.

Это действительно огромное число, но метод замыкания может завершить вычисление примерно за 100 мкс, тогда как рекурсия сталкивается со своими ограничениями.
Как ещё применять замыкание?
Замыкания Python очень полезны не только как замена рекурсивных функций. В некоторых случаях оно также может заменить классы Python на решение изящнее, особенно когда в классе не слишком много атрибутов и методов. Предположим, у нас есть словарь студентов с их экзаменационными отметками.
Хочется иметь несколько функций, которые помогут нам фильтровать студентов по оценкам, помещать их в разные классы. Однако со временем критерии могут измениться. В этом случае можно определить замыкание Python, вот так:
Замыкание определяет функцию, которая, в свою очередь, определяет другие функции на основе динамически передаваемых параметров. Мы передадим нижнюю и верхнюю границы класса оценки, и замыкание вернёт нам функцию, которая отфильтрует студентов.
Код выше даёт нам 4 функции, которые классифицируют учащегося по соответствующим классам на основе указанных нами границ. Обратите внимание, что мы можем изменить границу в любой момент, чтобы выполнить другую функцию или переопределить текущие функции оценки. Давайте теперь проверим функции.

Выглядит очень аккуратно! Но имейте в виду, что в более сложных случаях всё равно нужно определять классы.
Что в итоге?
В этой статье я представил технику, называемую замыканием, в Python. Её можно применить, чтобы переписать рекурсивные функций и в большинстве случаев значительно превзойти их.
В самом деле, замыкание может оказаться не лучшим решением некоторых проблем, с точки зрения производительности, особенно когда применимо динамическое планирование. Однако намного проще работать с ним в плане мышления. Иногда динамическое планирование излишне: например, когда не так уж важна производительность, может быть достаточно замыкания.
Замыкание также может использоваться, чтобы заменить некоторые юзкейсы, для удовлетворения которым мы можем захотеть реализовать класс. В таких случаях замыкание выглядит аккуратнее и элегантнее.
Узнайте подробности, как получить Level Up по навыкам и зарплате или востребованную профессию с нуля, пройдя онлайн-курсы SkillFactory со скидкой 40% и промокодом HABR, который даст еще +10% скидки на обучение: