Представьте: у вас есть десяток функций, и к каждой нужно добавить одно и то же — замерять время работы, печатать лог вызова, проверять права доступа. Лезть в тело каждой функции неудобно: код дублируется, а функция перестаёт заниматься одной задачей. Декораторы добавляют функции новое поведение снаружи, не трогая её код внутри. Но прежде нужно понять две вещи: что значит «функция как объект» и что такое замыкание — без них декоратор будет казаться магией.
Функции как объекты
Обычно вы думаете о функции как о команде: написали def, дали имя — и вызываете через скобки. Но в Python функция — это ещё и обычное значение, такое же, как число 5 или строка "привет". Её можно положить в переменную, передать в другую функцию как аргумент и даже вернуть как результат.
В документации это называется функции первого класса (first-class functions): значение, которое можно свободно передавать по программе, не вызывая. Именно на этом свойстве строятся декораторы: декоратор принимает функцию как значение и возвращает другую функцию как результат.
def greet(name):
return f"Привет, {name}!"
say_hello = greet
print(say_hello("Алиса"))
say_hello = greet— без скобок послеgreet. Скобки означали бы «вызови функцию сейчас». Без них — «сохрани саму функцию как объект». После этой строкиsay_helloиgreet— два имени одной функции.say_hello("Алиса")— скобки есть, значит функция вызывается. Через какое имя вызывать — неважно, результат одинаковый.- Вывод:
Привет, Алиса!
Теперь передадим функцию как аргумент в другую функцию:
def run_twice(func, value):
func(value)
func(value)
def shout(text):
print(text.upper())
run_twice(shout, "привет")
def run_twice(func, value):— параметрfuncничем не отличается отvalue. В него можно передать функцию так же, как число или строку.func(value)— внутриrun_twiceвызываем то, что лежит вfunc, передавая ейvalue.run_twice(shout, "привет")— передаём саму функциюshout(без скобок!) и строку. Внутриfuncстанет означатьshout, аvalue—"привет".- Вывод: дважды
ПРИВЕТ, потому чтоshoutпереводит текст в верхний регистр.
И последний вариант — функция может вернуть другую функцию:
def get_multiplier(n):
def multiply(x):
return x * n
return multiply
double = get_multiplier(2)
print(double(5))
def multiply(x):внутриget_multiplier— функция, объявленная прямо внутри другой функции.def— обычная инструкция, её можно писать где угодно, в том числе внутри тела другой функции.return multiply— без скобок: возвращаем саму функцию как объект, а не результат её вызова.double = get_multiplier(2)—get_multiplierсn = 2возвращает функциюmultiply, она сохраняется вdouble.double(5)— вызываем то, что лежит вdouble, передавая5. Сработаетx * n, гдеx = 5,n = 2.- Вывод:
10.
Загадка: get_multiplier уже завершилась, а double всё ещё помнит n = 2. Как? Ответ — в следующем разделе.
Замыкания: функция помнит своё окружение
Вложенная функция «видит» переменные внешней функции — так было с multiply, использовавшей n снаружи себя. Особенность в том, что эта связь не рвётся, даже когда внешняя функция уже закончила работу.
Официальный термин — замыкание (closure): вложенная функция вместе с переменными внешней, которые она использует. Python сохраняет эти переменные внутри самого объекта функции, поэтому они живут столько же, сколько она сама.
Пример счётчика:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
my_counter = make_counter()
print(my_counter())
print(my_counter())
print(my_counter())
count = 0— переменная в локальной области видимостиmake_counter.def counter():— вложенная функция, используетcount, хотя сама её не объявляла.nonlocal count— говорит Python:countвнутриcounterне новая переменная, а та, что во внешней функции. Без неёcount += 1вызвала бы ошибку.return counter— возвращаем саму функцию, не вызывая её. Функцияcounterунесла с собой ссылку наcountдаже после завершенияmake_counter— это и есть замыкание.- Три вызова
my_counter()печатают1,2,3— каждый разcountувеличивается на единицу.
nonlocal нужен только когда вложенная функция изменяет переменную из внешней (как count += 1). Для чтения без изменения он не нужен.
Первый декоратор без специального синтаксиса
Соединим функции как объекты и замыкания. Функция принимает другую функцию, создаёт третью — «обёртку» вокруг неё — и возвращает эту обёртку. Внутри обёртки можно выполнить что-то до вызова оригинала, вызвать сам оригинал и выполнить что-то после.
Официально такая функция называется декоратор (decorator): функция, принимающая функцию и возвращающая новую с добавленным поведением, не меняя код исходной.
def my_decorator(func):
def wrapper():
print("Перед вызовом")
func()
print("После вызова")
return wrapper
def say_hi():
print("Привет!")
say_hi = my_decorator(say_hi)
say_hi()
def my_decorator(func):— сам декоратор, принимает единственный аргументfunc— функцию, которую нужно украсить.def wrapper():— новая функция внутри декоратора, обёртка: она «оборачивает» вызов оригинала своим кодом до и после.func()внутриwrapper— вызывается оригинальная функция. Благодаря замыканиюwrapperпомнит, какая функция передана вfunc, даже вызываясь позже.return wrapper— декоратор возвращает саму обёртку, не результат её вызова.say_hi = my_decorator(say_hi)— получаемwrapperи переприсваиваем имяsay_hi. Теперь оно указывает наwrapper, а тот умеет вызвать старую функцию черезfunc.say_hi()— на самом деле вызываетсяwrapper. Вывод:Перед вызовом,Привет!,После вызова.
Синтаксис @decorator — просто сокращение
Писать say_hi = my_decorator(say_hi) отдельной строкой после каждой функции неудобно. В Python для этого есть синтаксис декоратора — символ @ перед объявлением функции. Это не отдельная возможность языка, а сокращённая запись той же строки func = decorator(func).
def my_decorator(func):
def wrapper():
print("Перед вызовом")
func()
print("После вызова")
return wrapper
@my_decorator
def say_hi():
print("Привет!")
say_hi()
@my_decoratorнадdef say_hi():означает: как только функция объявлена, сразу передай её вmy_decoratorи замени имя на результат. Происходит один раз, при объявлении, не при каждом вызове.- Результат идентичен предыдущему примеру:
Перед вызовом,Привет!,После вызова.
Никакой магии: @my_decorator над функцией — то же самое, что func = my_decorator(func) после неё, просто короче и не забудешь.
Декоратор для функции с аргументами и результатом
say_hi не принимала аргументов и ничего не возвращала — в жизни так почти не бывает. Декоратор должен украшать любую функцию. Знакомый *args и **kwargs собирает в обёртке любые аргументы, чтобы передать их дальше, в оригинал.
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"Вызов функции {func.__name__}")
result = func(*args, **kwargs)
print(f"Функция вернула: {result}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
total = add(3, 5)
print(total)
def wrapper(*args, **kwargs):— обёртка принимает любые позиционные (в кортежargs) и именованные (в словарьkwargs) аргументы, поэтому подходит для любой функции.func.__name__— встроенный атрибут со строкой имени функции, нужен для вывода.func(*args, **kwargs)вызывает оригинал, разворачивая собранное обратно в отдельные аргументы.return result— без этой строкиwrapperвернётNone, и декорированная функция всегда будет отдаватьNoneвместо настоящего результата.add(3, 5)запускаетwrapper(3, 5):argsстанет(3, 5). Вывод:Вызов функции add,Функция вернула: 8, затем8.
functools.wraps: сохраняем имя и docstring
У декорированной функции есть незаметный побочный эффект: имя и docstring подменяются на имя и docstring обёртки. Это мешает при отладке.
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__)
print(add.__doc__)
"""Складывает два числа."""подdef add— docstring, Python хранит её в атрибуте__doc__.add.__name__выведетwrapper, а неadd— имяaddтеперь указывает на объект-обёртку с собственным именем.add.__doc__выведетNone— уwrapperсвоего docstring нет.
Решение — functools.wraps из стандартной библиотеки: копирует __name__, __doc__ и другие метаданные оригинала на обёртку:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__)
print(add.__doc__)
@functools.wraps(func)— сам является декоратором, применяется кwrapperвнутри вашего. Принимаетfunc, чтобы знать, чьи метаданные копировать.- После этой строки
wrapper.__name__станет"add", аwrapper.__doc__— докстрингу оригинала. Поведение декоратора не меняется, меняются только служебные атрибуты. - Вывод теперь:
add, затемСкладывает два числа.
Правило на будущее: в любом своём декораторе добавляйте @functools.wraps(func) над wrapper. Стоит одной строки, а спасает от долгих поисков, почему help(моя_функция) показывает не то.
Декоратор с собственными аргументами
Иногда декоратор хочется настроить: например, @repeat(3) — повторить вызов функции трижды. Такая конструкция называется фабрикой декораторов (decorator factory): функция, принимающая аргументы настройки и возвращающая готовый декоратор. Три уровня вложенности вместо двух: внешняя принимает настройки, средняя — саму функцию (это и есть декоратор), внутренняя выполняет работу при вызове.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Привет!")
say_hello()
def repeat(times):— уровень 1, принимает настройку (число повторов) и готовит декоратор.def decorator(func):— уровень 2, настоящий декоратор: принимает функцию, возвращает обёртку. Видитtimesчерез замыкание.def wrapper(*args, **kwargs):— уровень 3: цикл вызываетfunc(*args, **kwargs)нужное число раз, перезаписываяresult, в нём остаётся результат последнего вызова.@repeat(3)надsay_hello: сначала вызываетсяrepeat(3), возвращаетdecoratorс зашитымtimes = 3, тот применяется кsay_hello. Вывод: три разаПривет!.
Несколько декораторов подряд
На функцию можно навесить сразу несколько декораторов, перечислив их построчно над def. Применяются они снизу вверх — тот, что ближе к def, срабатывает первым.
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return "**" + func(*args, **kwargs) + "**"
return wrapper
def uppercase(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@bold
@uppercase
def greet(name):
return f"привет, {name}"
print(greet("Алиса"))
boldоборачивает результат звёздочками,uppercaseпереводит его в верхний регистр — два независимых декоратора.@boldстоит выше,@uppercase— ближе кdef greet: эквивалентgreet = bold(uppercase(greet)).- При вызове первым срабатывает внешний
bold, он вызываетuppercase, тот — настоящийgreet:"привет, Алиса"→"ПРИВЕТ, АЛИСА"→"**ПРИВЕТ, АЛИСА**".
Применение декораторов к функции идёт снизу вверх (ближний к def — первый), а выполнение при вызове — сверху вниз (внешний срабатывает первым).
Практика: декоратор для замера времени
Декоратор timer узнаёт, сколько секунд работала функция, и печатает это:
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[{func.__name__}] выполнилась за {elapsed:.4f} сек")
return result
return wrapper
@timer
def slow_sum(n):
total = 0
for i in range(n):
total += i
return total
result = slow_sum(1_000_000)
print(f"Результат: {result}")
time.time()возвращает текущее время в секундах. Само число не важно — важна разница двух таких чисел, засечённых до и после вызоваfunc.elapsed = time.time() - startдаёт длительность выполнения;{elapsed:.4f}форматирует её с четырьмя знаками после запятой.resultсохранён и возвращён, как в разделе 5, иначеslow_sumвсегда возвращала быNone.
Практика: декоратор для логирования вызовов
Ещё один частый случай — печатать, с какими аргументами вызвали функцию и что она вернула, вместо ручного print в каждой функции:
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
print(f"Вызов: {func.__name__}({signature})")
result = func(*args, **kwargs)
print(f"Вернула: {result!r}")
return result
return wrapper
@log_calls
def divide(a, b):
return a / b
divide(10, 2)
[repr(a) for a in args]и[f"{k}={v!r}" for k, v in kwargs.items()]— генераторы списков собирают строковое представление каждого аргумента ({v!r}в f-строке — то же, чтоrepr(v)).", ".join(...)склеивает оба списка через запятую в строку вида10, 2. Вывод:Вызов: divide(10, 2), затемВернула: 5.0.
Практика: functools.lru_cache
Декоратор кэширования (запоминания уже посчитанных результатов) писать с нуля не нужно — он есть в стандартной библиотеке. functools.lru_cache запоминает, с какими аргументами функция уже вызывалась и что вернула, и при повторе не выполняет её заново.
Классический пример — рекурсивный подсчёт чисел Фибоначчи, где без кэша одни значения пересчитываются миллионы раз:
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35))
@functools.lru_cache(maxsize=None)— готовая фабрика декораторов, как и вашrepeat.maxsize=Noneхранит результаты без ограничения; число, например128, ограничило бы кэш 128 последними вызовами (LRU — Least Recently Used, давно неиспользуемый вытесняется первым).- При первом
fib(5)функция считает результат и запоминает пару «аргумент → результат». Повторный вызов с теми же аргументами вернёт сохранённое значение, не выполняя тело заново — поэтомуfib(35)с кэшем считается мгновенно, а без него — секунды.
Начиная с Python 3.9 есть более короткая запись: @functools.cache — то же самое, что @functools.lru_cache(maxsize=None), без ограничения размера кэша.
Частые ошибки
Забыли вернуть результат из wrapper
Если в wrapper написать func(*args, **kwargs) без return, декорированная функция всегда вернёт None: print(add(3, 5)) покажет None вместо 8.
Обёртка без *args, **kwargs
def wrapper(): без параметров подходит только для функций без аргументов. Декорировав так add(a, b), вызов add(3, 5) даст TypeError: wrapper() takes 0 positional arguments but 2 were given.
Вызов func вместо возврата func
return func() внутри самого декоратора (не внутри wrapper) вызовет функцию сразу при декорировании и вернёт её результат вместо новой функции. Последующий вызов упадёт с TypeError: 'int' object is not callable.
Декоратор с аргументами без третьего уровня
@repeat(3) требует три уровня (repeat → decorator → wrapper). С двумя, как в обычном декораторе, Python попытается использовать число 3 как саму функцию и упадёт с TypeError: 'int' object is not callable.
Что важно запомнить
- Функции в Python — такие же объекты, как числа и строки: их можно сохранять в переменных, передавать и возвращать
- Замыкание — вложенная функция, которая помнит переменные внешней функции даже после её завершения
- Декоратор — функция, которая принимает функцию и возвращает новую функцию с добавленным поведением
@decoratorнадdef— сокращённая записьfunc = decorator(func), применяется один раз при объявлении функции- Обёртка должна принимать
*args, **kwargsи всегда возвращатьresultоригинального вызова @functools.wraps(func)над обёрткой сохраняет её имя и docstring — ставьте в каждом своём декораторе- Декоратор с аргументами (
@repeat(3)) — три уровня вложенности: фабрика, декоратор, обёртка - Несколько декораторов применяются снизу вверх, а выполняются при вызове — сверху вниз
functools.lru_cache— готовый декоратор кэширования результатов из стандартной библиотеки
Проверьте себя
7 вопросов
Декоратор timer
Напиши декоратор timer, который замеряет время выполнения функции и печатает его в формате: Функция '{name}' выполнилась за {elapsed:.4f} сек.
Используй functools.wraps для сохранения имени и docstring оригинальной функции.
Подсказка: используй time.time() до и после вызова функции.