Всем привет! Меня зовут Михаил, я веду Telegram-канал «Python Шпильки», где делюсь изящными приемами программирования. Сегодня я хочу рассказать об универсальном декораторе, который может принимать аргументы, а также вызываться без их приема. Для тех кто хорошо знает тему декораторов - ничего нового они тут не увидят! Этот пост для тех, кто, возможно, хочет более подробно понять тему декоратора. Итак, поехали.

Для начала приведу пример конструкции универсального декоратора:

from functools import wraps
from typing import Callable, Any

def decorator(_func: Callable = None, *decorator_args, **decorator_kwargs) -> Callable:

    def decor_type(func: Callable) -> Callable:

        @wraps(func)
        def wrapped(*f_args, **f_kwargs) -> Any:

            print(f'Аргументы декоратора: {decorator_args} {decorator_kwargs}')
            func(*f_args, **f_kwargs)

            return func

        return wrapped

    if _func is None:

        return decor_type

    else:

        return decor_type(_func)



# эта функция написана для показа варианта аргумента, передаваемого в декоратор
def dop_function(text_print):

    return (text_print)




@decorator(None,1, dop_function('Привет'), 'рублей', 200, dop_function('Привет'), 'друзей', key_valid = True) # None - обязателен! при передаче даже 1 (одного) аргумента в декоратор!!!
def decorated_function(name: str, num: int) -> None:

    print('Привет', name, num)

decorated_function("Юзер", 10)

# Аргументы декоратора: (1, 'Привет', 'рублей', 200, 'Привет', 'друзей') {'key_valid': True}
# Привет Юзер 10

# Либо вызывать декоратор без аргументов:

@decorator
def decorated_function(name: str, num: int) -> None:

    print('Привет', name, num)

decorated_function("Юзер", 20)

# Аргументы декоратора: () {}
# Привет Юзер 20

Теперь, объяснение:

Этот код реализует универсальный декоратор, который может применяться как с аргументами, так и без них (согласно примерам внизу). Вот краткое объяснение ключевых частей:

  • Функция decorator принимает либо функцию _func, если декоратор применяется без аргументов, либо None и дополнительные позиционные и именованные аргументы (т.е *args, **kwargs) (ВНИМАНИЕ!: НЕ ЯВНО УКАЗАННЫЕ АРГУМЕНТЫ, НАПРИМЕР: name = 'Mike') для самого декоратора.

  • Внутренняя функция decor_type — это собственно декоратор, который принимает декорируемую функцию func.

  • Внутри decor_type функция-обёртка wrapped использует @wraps(func), чтобы сохранить оригинальное имя, документацию и аннотации функции.

  • wrapped выводит переданные аргументы декоратора (decorator_args и decorator_kwargs), затем вызывает исходную функцию с переданными ей позиционными и именованными аргументами.

  • При вызове decorator проверяется, была ли передана функция func. Если нет — возвращается decortype, ожидающий функцию; если _func есть — декорируется сразу.

Важно:

  • При применении декоратора только с позиционными и именованными аргументами требуется явно передать None первым аргументом, как в первом примере с @decorator(None, 1, ...). Это связано с тем, что без этого Python не сможет определить, что вы передаёте аргументы самому декоратору, а не декорируемой функции.

  • При использовании без аргументов можно применять просто @decorator, как во втором примере.

Пример демонстрирует два варианта использования:

  1. Передача аргументов декоратору (с None, чтобы указать, что будут параметры), после чего выводятся переданные аргументы и вызывается функция.

  2. Использование декоратора без аргументов —тогда decorator_args и decorator_kwargs пусты.

Каждый вызов выводит переданные в декоратор значения и сам результат работы функции.

Такой подход позволяет делать декоратор гибким и чистым, при этом сохраняя метаданные функций благодаря functools.wraps.

А теперь пример с применением не только позиционных и именованных аргументов. Он более полно покажет вам как использовать декоратор с разными видами аргументов. Итак, декоратор генерации HTML обертки (текста):

from functools import wraps
from typing import Callable, Any

from random import randint

def dop_text():
    text_print = ('На этой страничке вы можете просмотреть данные конкретного пользователя,'
                  'а также структуру и состав полей базы данных')
    return text_print




def decorator_html(_func: Callable = None, head = 'Привет!', text = 'Приветствую всех!', *decorator_args, **decorator_kwargs) -> Callable:

    def decor_type(func: Callable) -> Callable:

        @wraps(func)
        def wrapped(*f_args, **f_kwargs) -> Any:

            print(f'<h3>{head}</h3><p>{text}</p>')
            print('<table border=1><tr><td><strong>name</strong></td><td><strong>age</strong>'
                  '</td><td><strong>number</strong></td></tr>')
            name_user, age_user, id_user = func(*f_args, **f_kwargs)
            print(f'<tr><td>{name_user}</td><td>{age_user}</td><td>{id_user}</td>')

            return func

        return wrapped

    if _func is None:

        return decor_type

    else:

        return decor_type(_func)


# С прописанными именами аргументов !ВНИМАНИЕ: здесь None - уже не прописываем! Так как это у нас не позиционные и именнованые аргументы (не *args и **kwargs)!
@decorator_html(head='Данные пользователя', text = dop_text())
def decorated_function(name: str, age: int) -> None:

    # Здесь, допустим извлекаем данные из БД или API
    id_user = randint(10000, 12345)
    return name, age, id_user

decorated_function("Юзер", 10)

# Нюанс (без прописанных имен аргументов)
@decorator_html(None, 'Данные пользователя', dop_text()) # None - обязателен, если вы явно не указываете имя аргументов, например head='Привет'! при передаче даже 1 (одного) аргумента в декоратор!!!
# при таком варианте аргументы попадают в *args (либо в **kwargs)!
def decorated_function(name: str, age: int) -> None:

    # Здесь, допустим извлекаем данные из БД или API
    id_user = randint(10000, 12345)
    return name, age, id_user

decorated_function("Юзер", 20)

# Либо без аргументов

@decorator_html
def decorated_function(name: str, age: int) -> None:

    # Здесь, допустим извлекаем данные из БД или API
    id_user = randint(10000, 12345)
    return name, age, id_user

decorated_function("Юзер", 30)

Заключение

Это один из примеров универсального шаблона декоратора, который может принимать аргументы, а также вызываться и без них, не выдавая при этом ошибку. А какие универсальные декораторы используете Вы! Пишите в комментах. Всем Удачи!