Python славится своей гибкостью. Мы можем передавать функции как аргументы, возвращать их из других функций и даже "записывать" внутрь них состояние. Но как это работает под капотом? И при чем тут странное слово nonlocal?
В этой статье мы проследим эволюцию: начнем с глобальных переменных, разберемся с замыканиями (closures), поймем магию nonlocal, а затем соберем всё это вместе, чтобы понять, как работают декораторы — один из самых элегантных механизмов Python.
Часть 1. Глобальное vs Локальное: LEGB и первые проблемы
В Python есть четкая иерархия областей видимости — правило LEGB:
Local — локальная область функции
Enclosing — объемлющая функция (функция внутри функции)
Global — глобальная область модуля
Built-in — встроенные имена (print, len и т.д.)
Простой пример:
x = "глобальная" # Global def outer(): x = "объемлющая" # Enclosing def inner(): x = "локальная" # Local print(x) inner() print(x) outer() print(x)
Вывод:
локальная объемлющая глобальная
Всё логично: каждая функция видит "свою" переменную. Но что, если мы хотим изменить переменную из объемлющей функции внутри вложенной?
Часть 2. Nonlocal: Доступ к родителю
Представим счетчик, который должен считать вызовы функции:
def counter(): count = 0 def increment(): count += 1 # Ошибка! return count return increment c = counter() print(c())
Этот код упадет с UnboundLocalError: local variable 'count' referenced before assignment. Почему? Потому что count += 1 интерпретируется как создание новой локальной переменной count внутри increment, а не использование внешней.
Решение — nonlocal:
def counter(): count = 0 def increment(): nonlocal count # Говорим: "используй переменную из объемлющей функции" count += 1 return count return increment c1 = counter() print(c1()) # 1 print(c1()) # 2 print(c1()) # 3 c2 = counter() # Новый счетчик с нуля print(c2()) # 1
nonlocal поднимается на уровень выше (но не до глобальной!). Для глобальных переменных есть global.
Аналогия: nonlocal — это как ключ от комнаты родителей. Вы можете войти и изменить там вещи, но вы все еще не на улице (глобальной области).
Часть 3. Замыкания (Closures): Функция, которая помнит
А теперь посмотрим на пример выше. Функция counter отработала и завершилась. Переменная count должна была бы умереть. Но функция increment, которую мы вернули, продолжает иметь к ней доступ!
Это и есть замыкание (closure) — функция, которая "захватила" переменные из своей внешней области видимости и продолжает их видеть даже после завершения внешней функции.
Как это выглядит изнутри:
def make_multiplier(n): def multiplier(x): return x * n return multiplier double = make_multiplier(2) triple = make_multiplier(3) print(double(5)) # 10 print(triple(5)) # 15 # Магия: смотрим, что хранит функция print(double.__closure__[0].cell_contents) # 2 print(triple.__closure__[0].cell_contents) # 3
У каждой функции есть атрибут closure, который хранит ячейки с захваченными переменными. double и triple — это не просто функции, это функции с личными данными.
Где используются замыкания:
Фабрики функций (как в примере выше)
Сохранение состояния без классов (счетчики, кэши)
Декораторы (о них дальше)
Часть 4. Подводные камни замыканий
Позднее связывание (Late Binding)
Классическая ловушка с лямбдами в циклах:
funcs = [] for i in range(3): funcs.append(lambda: i) for f in funcs: print(f()) # 2, 2, 2 (а ожидали 0, 1, 2)
Почему? Лямбды захватили переменную i, но не ее значение. К моменту вызова функций цикл уже завершился и i равна 2.
Решение через аргумент по умолчанию (фиксация значения):
funcs = [lambda x=i: x for i in range(3)] # Значение i фиксируется в момент создания
Решение через nonlocal (редко):
def make_func(i): return lambda: i funcs = [make_func(i) for i in range(3)] # Создаем отдельную область видимости
Часть 5. Декораторы: Замыкания в действии
Декоратор — это функция, которая принимает другую функцию и возвращает ее модифицированную версию. И это чистое замыкание!
Простейший декоратор:
def logger(func): def wrapper(*args, **kwargs): print(f"Вызов функции {func.__name__} с аргументами {args} {kwargs}") result = func(*args, **kwargs) print(f"Результат: {result}") return result return wrapper @logger def add(a, b): return a + b add(2, 3) # Вызов функции add с аргументами (2, 3) {} # Результат: 5
Что здесь произо��ло:
@logger— это синтаксический сахар дляadd = logger(add)loggerпринимает функциюaddи возвращает функциюwrapperwrapper— замыкание, которое захватилоfunc(исходную функцию)Теперь имя
addуказывает наwrapper, который перед вызовом печатает логи
Декоратор с аргументами
Иногда нужно параметризовать декоратор. Тогда создается еще один уровень вложенности:
def repeat(n): def decorator(func): def wrapper(*args, **kwargs): for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def say_hello(): print("Привет!") say_hello() # Привет! Привет! Привет!
Разбираем по уровням:
repeat(3)возвращаетdecorator@decoratorприменяется к функцииdecoratorвозвращаетwrapperwrapper— замыкание, захватившееfuncиn
Часть 6. Сохраняем лицо: @wraps
Есть проблема: декорированная функция теряет свое имя и документацию.
@logger def add(a, b): """Складывает два числа""" return a + b print(add.__name__) # wrapper print(add.__doc__) # None
Решение — functools.wraps:
from functools import wraps def logger(func): @wraps(func) # Копирует имя, docstring и другие атрибуты def wrapper(*args, **kwargs): print(f"Вызов {func.__name__}") return func(*args, **kwargs) return wrapper @logger def add(a, b): """Складывает два числа""" return a + b print(add.__name__) # add print(add.__doc__) # Складывает два числа
@wraps — это декоратор для декоратора, который обновляет метаданные.
Часть 7. Несколько декораторов: Матрешка
Порядок применения декораторов важен:
def bold(func): @wraps(func) def wrapper(): return f"<b>{func()}</b>" return wrapper def italic(func): @wraps(func) def wrapper(): return f"<i>{func()}</i>" return wrapper @bold @italic def hello(): return "Привет" print(hello()) # <b><i>Привет</i></b>
Работает снизу вверх: сначала italic, потом bold. То есть hello = bold(italic(hello)).
Часть 8. Класс как декоратор
Декоратором может быть не только функция, но и класс — через метод call:
class Counter: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"Функция вызвана {self.count} раз") return self.func(*args, **kwargs) @Counter def say(word): print(word) say("Привет") # Функция вызвана 1 раз say("Пока") # Функция вызвана 2 раз
Здесь класс хранит состояние в атрибуте count — это альтернатива замыканию.
Часть 9. Реальные примеры декораторов
Таймер
import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} выполнилась за {end-start:.4f} сек") return result return wrapper @timer def slow_func(): time.sleep(1)
Кэширование (мемоизация)
def cache(func): memo = {} @wraps(func) def wrapper(n): if n not in memo: memo[n] = func(n) return memo[n] return wrapper @cache def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(40)) # Работает быстро благодаря кэшу
Проверка прав доступа
def requires_permission(permission): def decorator(func): @wraps(func) def wrapper(user, *args, **kwargs): if permission not in user.permissions: raise PermissionError(f"Нет права {permission}") return func(user, *args, **kwargs) return wrapper return decorator @requires_permission("admin") def delete_user(user, target_id): print(f"Пользователь {target_id} удален")
Часть 10. Встроенные декораторы
Python поставляется с готовыми декораторами:
Декоратор | Назначение |
|---|---|
| Метод, не получающий self |
| Метод, получающий класс вместо self |
| Метод, который вызывается как атрибут |
| Кэширование результатов |
| Генерация |
Заключение: Цепочка понятий
Мы прошли путь:
Области видимости → узнали, где живут переменные
nonlocal→ научились изменять переменные из объемлющей функцииЗамыкания → поняли, как функция может хранить состояние
Декораторы → применили замыкания для модификации функций
Эти концепции — не просто теория. Они используются в каждом втором фреймворке (Flask, Django, FastAPI), в библиотеках для логирования, кэширования, валидации. Понимая их, вы перестаете видеть магию и начинаете видеть инструменты.
Главный вывод: Замыкания и декораторы — это способ писать код, который изменяет код. Это метапрограммирование в действии, и теперь вы знаете, как оно работает изнутри.
Если тема функций в Python вам интересна, больше практических примеров и разборов вы найдете на моем курсе на Stepik.
В моем Telegram-канале я публикую дополнительные материалы по Python и backend-разработке. Там вы можете найти разборы тем, практические примеры, объяснения сложных концепций простым языком и продолжения подобных статей.
