Перейти к содержанию

252 уроков, 17 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 17 из 20 Средний 40 мин 150 XP

Декораторы: функции над функциями

Декоратор — функция, которая принимает функцию и возвращает новую, добавляя поведение без правки исходного кода. Соберём декоратор с нуля: от замыкания и синтаксиса @ до functools.wraps, декораторов для методов и декораторов с параметрами.

Редакция Питоники

В Python функция — обычный объект: её можно положить в переменную, передать аргументом и вернуть из другой функции. Вы уже пользовались этим не замечая: sorted(data, key=len) получает функцию len, а @property и @staticmethod из прошлых уроков — декораторы. Теперь соберём свой. Сначала — фундамент: функция, возвращающая функцию, которая помнит окружение (замыкание):

  • sorted(data, key=len) — функция len передаётся как обычный аргумент;
  • объекты dict.get, str.upper — тоже значения: их можно передавать и вызывать позже;
  • @property, @staticmethod, @functools.cache — декораторы, которыми вы уже пользовались как готовыми деталями.

Функция как значение и замыкание

функция возвращает функцию с памятью
def make_greeting(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

hello = make_greeting("Привет")
bye = make_greeting("Пока")

print(hello("Анна"))
print(bye("Пётр"))
Вывод
Привет, Анна!
Пока, Пётр!
декоратор: принимаем функцию, возвращаем обёртку
def shout(fn):
    def wrapper():
        return fn().upper() + "!"
    return wrapper

def greet():
    return "привет"

loud = shout(greet)
print(loud())

@shout
def farewell():
    return "удачи"

print(farewell())
Вывод
ПРИВЕТ!
УДАЧИ!

functools.wraps: сохранить имя и docstring

wraps + *args/**kwargs — рабочая схема декоратора
import functools

def logged(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        result = fn(*args, **kwargs)
        print(f"вызвана {fn.__name__}{args} -> {result}")
        return result
    return wrapper

@logged
def add(a, b):
    """Складывает два числа."""
    return a + b

add(2, 3)
print(add.__name__)
print(add.__doc__)
Вывод
вызвана add(2, 3) -> 5
add
Складывает два числа.

Декоратор метода и декоратор с параметрами

проверка аргументов декоратором метода
def positive(fn):
    def wrapper(self, value):
        if value < 0:
            raise ValueError("сумма должна быть неотрицательной")
        return fn(self, value)
    return wrapper

class Account:
    def __init__(self):
        self.balance = 0

    @positive
    def deposit(self, value):
        self.balance += value

acc = Account()
acc.deposit(100)
acc.deposit(50)
print(acc.balance)

try:
    acc.deposit(-10)
except ValueError as e:
    print(e)
Вывод
150
сумма должна быть неотрицательной
декоратор с параметрами: функция, возвращающая декоратор
import functools

def repeat(times):
    def deco(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return wrapper
    return deco

@repeat(times=3)
def beep():
    print("бип")

beep()
Вывод
бип
бип
бип
Декораторов стало три уровня, потому что @repeat(times=3) сначала вычисляет repeat(times=3) — и только вернувшийся deco оборачивает функцию. Несколько декораторов над одной функцией применяются снизу вверх: @a над @b даёт f = a(b(f)).
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def decorate(fn):
    def wrapper():
        return "[" + fn() + "]"
    return wrapper

@decorate
def hello():
    return "py"

print(hello())
def double(fn):
    def wrapper(*args):
        return fn(*args) * 2
    return wrapper

@double
def square(x):
    return x * x

print(square(5))
import functools

def announce(fn):
    @functools.wraps(fn)
    def wrapper(*args):
        print(f"-> {fn.__name__}")
        return fn(*args)
    return wrapper

@announce
def mul(a, b):
    return a * b

print(mul(4, 6))
Проверь себя
0 / 6

1. Что делает запись @shout над функцией farewell?

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

3. Зачем декоратору @functools.wraps(fn) над wrapper?

4. Почему в wrapper декоратора метода первым параметром стоит self?

5. Чем декоратор с параметрами отличается от обычного?

6. Два декоратора применены в порядке @a над @b. Чему эквивалентно описание функции f?

Карточки терминов
Запомнено: 0 / 7
Практика

Напишите декоратор announce: перед вызовом декорированной функции он печатает строку вида «Вызываю add», затем возвращает результат функции. Используйте functools.wraps. Примените его к функции add(a, b), возвращающей сумму, и вызовите add(20, 22).

practice.py
Вопросы и ответы по уроку

Что такое декоратор в Python простыми словами?

Это функция, которая принимает вашу функцию и возвращает обёртку вокруг неё: логирование, проверку аргументов, кэширование — без правки исходного кода. Запись @deco над функцией — сокращение для func = deco(func).

Урок 17: декораторы

Зачем в декораторе functools.wraps?

Без него декорированная функция теряет имя и docstring: __name__ становится wrapper, а __doc__ — None, что ломает help(), отладку и документацию. @functools.wraps(fn) копирует метаданные исходной функции на обёртку — ставьте его первой строкой в каждом декораторе.

Как написать декоратор с параметрами?

Добавьте ещё один уровень: функция принимает параметры (например, repeat(times)) и возвращает обычный декоратор deco, который возвращает wrapper. Запись @repeat(times=3) сначала вычисляет repeat(times=3), и вернувшийся декоратор оборачивает функцию.

Можно ли декорировать методы класса?

Да: wrapper первым параметром принимает self, потому что после @deco имя метода указывает на wrapper, и вызов obj.method(x) передаёт ему obj как первый аргумент. Так работают, например, проверки аргументов и кэширование на уровне метода — как positive для deposit из урока.

Понравился урок? Сошлитесь на него

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.