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-сценарий с ожидаемым поведением.
- Называет хотя бы один риск: производительность, безопасность, транзакции, память или сопровождение.