PythonJuniorCoding

Как работают замыкания (closures) в Python? Приведите пример.

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

Как работают замыкания

Когда вложенная функция обращается к переменной из объемлющего scope, Python «захватывает» эту переменную в cell-объект. Cell живёт дольше фрейма внешней функции и доступен через атрибут __closure__ вложенной функции. Это и есть замыкание (closure).

Базовый пример

def make_multiplier(factor: int):  # внешняя функция
    def multiply(x: int) -> int:   # замыкание захватывает factor
        return x * factor
    return multiply


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

print(double(10))   # 20
print(triple(10))   # 30

# Посмотреть захваченные переменные:
print(double.__closure__)          # (<cell at 0x...>,)
print(double.__closure__[0].cell_contents)  # 2

Практический пример: фабрика логгеров

import logging
from typing import Callable


def make_logger(module_name: str) -> Callable[[str], None]:
    logger = logging.getLogger(module_name)  # захватывается замыканием

    def log(message: str) -> None:
        logger.info("[%s] %s", module_name, message)

    return log


auth_log = make_logger("auth")
jobs_log = make_logger("jobs")

auth_log("user logged in")    # [auth] user logged in
jobs_log("new job posted")    # [jobs] new job posted

Классическая ловушка: замыкание в цикле

# НЕВЕРНО: все функции захватят одну и ту же переменную i
funcs_bad = [lambda: i for i in range(3)]
print([f() for f in funcs_bad])  # [2, 2, 2] — не [0, 1, 2]!

# ПРАВИЛЬНО: зафиксировать значение через default-аргумент
funcs_ok = [lambda i=i: i for i in range(3)]
print([f() for f in funcs_ok])   # [0, 1, 2]

# Или через фабрику
def make_func(i: int):
    def f() -> int:
        return i
    return f

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

nonlocal — изменение захваченной переменной

def make_counter(start: int = 0):
    count = start

    def increment(by: int = 1) -> int:
        nonlocal count   # без nonlocal — UnboundLocalError
        count += by
        return count

    def reset() -> None:
        nonlocal count
        count = start

    return increment, reset


inc, rst = make_counter(10)
print(inc())    # 11
print(inc(5))   # 16
rst()
print(inc())    # 11

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

# Замыкание — лаконично для простого состояния
def rate_limiter(max_calls: int, window: float):
    import time
    calls: list[float] = []

    def allow() -> bool:
        now = time.monotonic()
        # Удаляем вызовы вне окна
        while calls and now - calls[0] > window:
            calls.pop(0)
        if len(calls) < max_calls:
            calls.append(now)
            return True
        return False

    return allow


check = rate_limiter(max_calls=3, window=1.0)
for _ in range(5):
    print(check())  # True True True False False

Подводные камни

  • Замыкание в цикле захватывает ссылку на переменную, а не её значение в момент создания — классическая ошибка с lambda в цикле.
  • Без nonlocal попытка присвоить значение захваченной переменной внутри вложенной функции вызывает UnboundLocalError.
  • Захваченные объекты удерживаются в памяти пока живёт замыкание — это может привести к утечкам при хранении больших объектов (DataFrame, открытые файлы).
  • Замыкания не сериализуются через pickle по умолчанию — проблема при использовании с multiprocessing.
  • Глубокое вложение замыканий ухудшает читаемость; если состояний много — лучше использовать класс.
  • Дебаггинг замыканий сложнее: трейсбек не показывает захваченные переменные явно; используйте fn.__closure__[i].cell_contents.

Common mistakes

  • Описывать closures только как термин и не показывать механизм на минимальном примере.
  • Игнорировать ошибки, пустые данные, конкурентный доступ или границы транзакции.
  • Не связывать поведение с официальным контрактом Python и реальной эксплуатацией.

What the interviewer is testing

  • Объясняет closures через последовательность действий, а не через набор ключевых слов.
  • Приводит короткий кодовый пример или production-сценарий с ожидаемым поведением.
  • Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.

Sources

Related topics