Вселенная программирования. Ключевые концепции ч2 - Замыкания.
Серия тем о вселенной программирования продолжается. В предыдущей своей статье я описывал основные концепции программирования - то, что является фундаментальной идейной базой, то, на чем основывается искусство программирования. Данные идеи практически реализованы в языках программирования в различных комбинациях. И в той же статье рассказал об одной из четырех ключевых концепций - концепции именованного состояния.
Итак, сегодня речь пойдет о еще одной базовой концептуальной вещи в программировании - замыкания, а если быть точнее, замыкания с лексической областью видимости (lexically scoped closure).
Это достаточно мощная концепция сама по себе. Например, функциональное программирование, по сути, и есть воплощение концепции замыкания. На данном этапе понятно словесно описать суть данной идеи достаточно тяжело, поэтому я для начала приведу пример кода с пояснениями.
Самой наглядной и популярной реализацией замыкания являются классические функции.
main_func
, которая возвращает inner_func
, которая обращается к локальной переменной name
, определённой внутри main_func
.
Когда вызвана main_func
, она возвращает inner_func
, которая запоминается где-то во внешней переменной (например, f = main_func()
), после чего main_func
как бы прекращает своё существование.
Однако inner_func
, если её теперь вызывать через переменную f()
, должна использовать внутри себя переменную name
, принадлежащую функции main_func
, которая уже не существует.
Замыкание - это функция inner_func
, которая связана со своим контекстом выполнения. Она замкнула в себе определённую информацию из внешней среды в пределах своей области видимости, и, хотя main_func
формально не существует, inner_func
будет корректно использовать переменную name
, локальную внутри конкретного экземпляра функции inner_func
.
Вот еще пример функции счетчика, реализованной при помощи замыкания:
def counter():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
c = counter()
for i in range(5):
print(c())
# 1
# 2
# 3
# 4
# 5
r = counter()
for i in range(3):
print(r())
# 1
# 2
# 3
Каждый раз при вызове функции counter()
, она будет считать и запоминать кол-во своих вызовов.
Если создать новый объект (при этом пространство имен обновится), то счетчик сбрасывается и считает заново.
Ну а теперь попробую описать суть концепции более простыми словами.
Итак, замыкание – это функция, связанная со своим контекстом выполнения использованием пространства имен. Главная идея тут заключается в разделении определения программной логики от ее исполнения через создание функций, которые возвращают своим результатом другие функции со своим контекстом. Другими словами, программа может взять некоторые инструкции кода в одном месте в виде отложенного “пакета работы”, передать их в другое место, и исполнить уже там. Причём результат выполнения пакета в новом месте программы будет точно таким же, как если бы этот пакет выполнялся там, где замыкание было исходно создано.
Кроме того, объекты и другие программные компоненты, хранящие внутри себя нужные состояния (или ссылки на них) – это также является замыканиями. Например, в ООП класс – это, по сути, функция, которая при своем вызове (через конструктор) возвращает объект – это тоже замыкание.
Таким образом, мы видим в концепции замыкания общую ключевую конструкцию для различных парадигм программирования, таких как функциональное, императивное и объектно-ориентированное. В большинстве языков программирования замыкания скрыты внутри имплементации языка и не доступны программисту напрямую.