ПрограммированиеBackend разработчик

Объясните, как работают замыкания (closures) в Python, чем они отличаются от обычных функций, и каково их практическое применение?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса

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

Проблема

В Python функции — объекты первого класса. Иногда требуется, чтобы вложенная функция использовала переменные из области видимости enclosing-функции даже после её завершения. Обычная лексическая область видимости этого не гарантирует при возврате функции. Если такая функция обращается к переменным среды своего создания — возникает замыкание.

Решение

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

Пример кода:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

Ключевые особенности:

  • Замыкание хранит значения переменных окружения, даже если внешняя функция уже завершилась.
  • Состояние во вложенной функции по сути приватно, его нельзя изменить напрямую снаружи.
  • Для изменения не-immutable переменной во внешней функции внутри closure используется ключевое слово nonlocal.

Вопросы с подвохом.

Может ли замыкание сохранять изменяемое состояние между вызовами, если переменная изменяется внутри вложенной функции?

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

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

Можно ли реализовать приватные переменные в Python, используя замыкания, вместо классов?

Да, closure предлагает простую реализацию "приватных" переменных, недоступных извне, если не предоставлены геттеры/сеттеры во вложенной функции.

Применяются ли замыкания только к функциям? Можно ли организовать closure с лямбдами в Python?

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

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

Типовые ошибки и анти-паттерны

  • Ожидать, что closure автоматически изменяет внешнюю переменную при её изменении внутри без nonlocal.
  • Захватывать изменяемые объекты в closure и мутировать их, не подозревая о неудобствах при отладке.
  • Использовать цикл для создания функций в замыкании без правильной привязки переменных (ловушка "все функции видят последнее значение переменной").

Пример из жизни

Негативный кейс

Фабрика функций, формирующая обработчики в цикле, использует переменную цикла внутри closure:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

Плюсы:

  • Просто, мало кода.

Минусы:

  • Все обработчики ссылаются на одну и ту же переменную i, её последнее значение 2 — поведение неожиданное для большинства.

Позитивный кейс

Использован аргумент по умолчанию, чтобы "зафиксировать" значение:

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

Плюсы:

  • Привязка необходимого значения.
  • Предсказуемое поведение.

Минусы:

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