W3docs

Замыкания в Python

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

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

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

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

Когда Python выполняет функцию, он создаёт локальную область видимости, которая исчезает после возврата функции. Обычно все определённые там переменные уничтожаются. Замыкание — исключение: если внутренняя функция ссылается на переменную из внешней функции, Python сохраняет эту переменную живой в специальном объекте-ячейке (cell object), а внутренняя функция несёт ссылку на эти ячейки куда бы ни передавалась.

Простейшее замыкание — это фабрика функций: функция, которая создаёт и возвращает другую функцию:

def make_multiplier(factor):
    def multiply(n):
        return n * factor   # 'factor' is captured from the enclosing scope
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15
print(double(10))  # 20

Каждый вызов make_multiplier создаёт новое замыкание с собственной независимой копией factor. double и triple полностью независимы, несмотря на то что были созданы одной и той же функцией.

Три условия для замыкания

Функция является замыканием, когда выполняются все три условия:

  1. Существует вложенная функция — функция, определённая внутри другой функции.
  2. Вложенная функция ссылается на переменную из охватывающей области видимости — такая переменная называется свободной переменной.
  3. Охватывающая функция возвращает вложенную функцию (или передаёт её куда-либо ещё).
def outer():
    message = 'Hello from outer'  # free variable
    def inner():
        print(message)            # inner references it
    return inner                  # outer returns inner

greet = outer()
greet()   # Hello from outer

После того как outer() возвращает управление, её локальный фрейм уничтожается — но message остаётся живым внутри greet.__closure__.

Исследование замыкания

Python открывает доступ к ячейкам замыкания через атрибут __closure__:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
print(add5(3))                            # 8
print(add5.__closure__)                   # (<cell at 0x...>,)
print(add5.__closure__[0].cell_contents)  # 5

__closure__ — это кортеж объектов-ячеек, по одной на каждую захваченную переменную. Если функция не является замыканием, __closure__ равно None.

Изменение захваченных переменных с помощью nonlocal

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

def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count      # rebind the enclosing 'count', not a new local
        count += step
        return count
    return increment

counter = make_counter()
print(counter())    # 1
print(counter())    # 2
print(counter(5))   # 7

counter2 = make_counter(10)
print(counter2())   # 11
print(counter())    # 8  — counter is unaffected

Каждый вызов make_counter создаёт независимую ячейку count. counter и counter2 не разделяют состояние.

Для более глубокого изучения того, как Python определяет, к какой области видимости принадлежит переменная, смотрите Область видимости в Python.

Типичная ошибка: замыкания в циклах

Классическая ошибка — создание замыканий внутри цикла с ожиданием, что каждое из них захватит текущее значение переменной цикла:

# Wrong — all functions capture the same 'i' cell
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2]  — not [0, 1, 2]

Все три лямбды разделяют одну ячейку, в которой хранится переменная цикла i. К моменту их вызова i достигло своего конечного значения 2.

Исправление 1: аргумент по умолчанию (захват по значению)

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)   # default arg is evaluated immediately

print([f() for f in funcs])  # [0, 1, 2]

Исправление 2: функция-фабрика

def make_func(i):
    def f():
        return i
    return f

funcs = [make_func(i) for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

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

Практические сценарии использования

Частичное применение

Замыкания — лёгковесная альтернатива functools.partial, когда нужна предварительно настроенная версия функции:

def make_power(exponent):
    def power(base):
        return base ** exponent
    return power

square = make_power(2)
cube   = make_power(3)

print(square(4))  # 16
print(cube(3))    # 27

Простое мемоизирование

Замыкание может содержать словарь-кэш, который сохраняется между вызовами:

def make_memoized(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@make_memoized
def slow_square(n):
    return n * n

print(slow_square(4))   # 16
print(slow_square(4))   # 16 (served from cache)
print(slow_square(7))   # 49

Этот паттерн — именно то, как работают декораторы Python под капотом: декоратор — это просто замыкание, которое оборачивает другую функцию.

Настройка коллбэков

Замыкания удобны для создания коллбэков, которым нужен некоторый встроенный контекст:

def make_logger(prefix):
    def log(message):
        print(f'[{prefix}] {message}')
    return log

info  = make_logger('INFO')
error = make_logger('ERROR')

info('Server started')    # [INFO] Server started
error('Disk full')        # [ERROR] Disk full

Замыкания vs. классы

Замыкание и класс с единственным методом часто решают одну и ту же задачу. Выбирайте, исходя из сложности:

СитуацияПредпочтительный подход
Одно состояние, одно поведениеЗамыкание
Несколько методов или публичных атрибутовКласс
Требуется сериализация (например, pickle)Класс
Передача коллбэка другой функцииЗамыкание
# Class approach
class Counter:
    def __init__(self, start=0):
        self.count = start
    def increment(self, step=1):
        self.count += step
        return self.count

# Closure approach
def make_counter(start=0):
    count = start
    def increment(step=1):
        nonlocal count
        count += step
        return count
    return increment

Оба подхода дают идентичное поведение. Замыкание короче; класс более очевиден и расширяем.

Заключение

Замыкания позволяют вложенной функции нести собственное приватное состояние, запоминая переменные из области видимости, в которой она была определена. Ключевые моменты:

  • Замыкание требует вложенной функции, свободной переменной и того, чтобы внутренняя функция была возвращена или передана дальше.
  • Используйте nonlocal, когда нужно переприсвоить (а не просто прочитать) захваченную переменную.
  • Избегайте ошибки с переменной цикла: используйте функцию-фабрику или аргумент по умолчанию, чтобы зафиксировать значение на каждой итерации.
  • Замыкания лежат в основе декораторов и тесно связаны с правилами области видимости Python.

Практика

Практика
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Which keyword allows an inner function to rebind a variable from its enclosing scope?
Практика
What does the __closure__ attribute return when a function is NOT a closure?
What does the __closure__ attribute return when a function is NOT a closure?
Was this page helpful?