Всем привет! Меня зовут Михаил, я веду 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, как во втором примере.
Пример демонстрирует два варианта использования:
Передача аргументов декоратору (с None, чтобы указать, что будут параметры), после чего выводятся переданные аргументы и вызывается функция.
Использование декоратора без аргументов —тогда 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)
Заключение
Это один из примеров универсального шаблона декоратора, который может принимать аргументы, а также вызываться и без них, не выдавая при этом ошибку. А какие универсальные декораторы используете Вы! Пишите в комментах. Всем Удачи!