Декораторы в Python
Введение
Нужно предварительно изучить темы функции первого класса и замыкания
Декораторы функций — вызываемые объекты, которые принимают другую функцию в качестве аргумента.
Декораторы функций могут производить операции с функцией и возвращают либо саму функцию, либо другую заменяющую её функцию или вызываемый объект.
То есть, если в коде ранее был прописан декоратор, названный my_decorator, то следующий код
@my_decorator def my_func():
Означает, что функция обёрнута в декоратор.
Первым делом Python обрабатывает функцию, которая завёрнута в декоратор. Получается объект функции.
Этот объект передаётся в функцию декоратор.
Декоратор возвращает изменённый объект функции обратно. Происходит новая связь между именем функции
и объектом. То есть теперь функция my_func будет называться по-прежнему my_func но работать
в соответствии с изменениями, внесёнными декторатором.
Начиная с версии Python 3.9 декоратором может быть любое валидное выражение. Подробнее в
PEP 614
Пример
Создайте файл
decorators.py
Внутри нужно создать функцию my_my_decorator() которая принимает в качестве аргумента
функцию, и функцию display(), которая пока не принимает никаких аргумнетов - её
мы будем декорировать.
Следующий пример не добавляет никакого функционала - вызов недекорированной и декорированной функций приводит
к выводу одного и того же сообщения.
# decorators.py def my_decorator(original_function): def wrapper(): return original_function() return wrapper def display(): print('display function ran') # Простой вызов функции display() # Вызов с использованием декоратора decorated_display = my_decorator(display) decorated_display()
python decorators.py
display function ran
display function ran
Чтобы декоратор делал хотя бы что-то видимое добавим в него вывод текстового сообщения.
def my_decorator(original_function): def wrapper_my_decorator(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper_my_decorator def display(): print('display function ran') # Простой вызов функции display() # Вызов с использованием декоратора decorated_display = my_decorator(display) decorated_display()
python decorators.py
display function ran
wrapper executed this before display
display function ran
Запись декоратора через @
Следующие две записи идентичны по смыслу
# 1 @my_decorator def my_func(): print('Hello') # 2 def my_func(): print('Hello') my_func = my_decorator(my_func)
Подразумевается, что декоратор my_decorator() существует
def my_decorator(func): print("Decorating") def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper # 1 @my_decorator def my_func(): print('Hello') my_func() # 2 def my_func(): print('Hello') my_func = my_decorator(my_func) my_func()
Decorating Hello Decorating Hello
Декорирование функции без параметров
Более привычным будет следующее оформление декоратора
def my_decorator(original_function): def wrapper_my_decorator(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper_my_decorator @my_decorator def display(): print('display function ran') display()
python decorators.py
wrapper executed this before display
display function ran
Следующие две записи идентичны по смыслу
# 1 @my_decorator def display(): print('display function ran') # 2 def display(): print('display function ran') display = my_decorator(display)
Пример
Рассмотрим функцию, которая возвращает название города, в котором содержатся умлауты
def northen_city(): return 'Tromsø' print(northen_city())
Tromsø
Напишем декоратор, который будет заменять умлаут на его номер в ASCII
def escape_unicode(f): def wrap(): return ascii(f()) return wrap @escape_unicode def northen_city(): return 'Tromsø' print(northen_city())
'Troms\xf8'
Так как функция не принимает аргументы, достаточно было просто обернуть её вызов в вызов ascii()
Если бы функция northen_city() принимала бы аргумент, то предудущий декоратор не справился бы
def escape_unicode(f): def wrap(): return ascii(f()) return wrap @escape_unicode def northen_city(city): return 'Northen city ' + city print(northen_city("Tromsø"))
Traceback (most recent call last): File "C:\Users\Andrei\dec.py", line 18, in <module> print(northen_city("Tromsø")) ^^^^^^^^^^^^^^^^^^^^^^ TypeError: escape_unicode.<locals>.wrap() takes 0 positional arguments but 1 was given
Про декорирование этой функции вы можете прочитать здесь либо последовательно прочитать следующую главу - «Декорирование функции с параметрами»
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги
Декорирование функции с параметрами
Рассмотрим функцию display() без параметров и функцию display_info(), которая принимает два аргумента.
def display(): print('display function ran') def display_info(name, age): print('display_info ran with arguments ({}, {})'.format(name, age)) display() display_info('Ivan', 25)
python decorators.py
display function ran
display_info ran with arguments (Ivan, 25)
Если применить декоратор из первого примера к обеим функциям будет ошибка
def my_decorator(original_function): def wrapper_my_decorator(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper_my_decorator @my_decorator def display(): print('display function ran') @my_decorator def display_info(name, age): print('display_info ran with arguments ({}, {})'.format(name, age)) display_info('Ivan', 25)
python decorators.py
Traceback (most recent call last): File "/home/andrei/python/decorators.py", line 17, in <module> display_info('Ivan', 25) TypeError: wrapper_my_decorator() takes 0 positional arguments but 2 were given
Если решить эту проблему добавилением двух аргументов в декоратор
def my_decorator(original_function): def wrapper_my_decorator(name, age): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function(name, age) return wrapper_my_decorator
То с display_info(name, age) он будет работать а с display() уже нет - эти аргументы лишние и функция их не ждёт
TypeError: wrapper_my_decorator() missing 2 required positional arguments: 'name' and 'age'
Сделать декоратор универсальным можно воспользовавшись *args, **kwargs
def my_decorator(original_function): def wrapper_my_decorator(*args, **kwargs): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function(*args, **kwargs) return wrapper_my_decorator @my_decorator def display(): print('display function ran') @my_decorator def display_info(name, age): print('display_info ran with arguments ({}, {})'.format(name, age)) display_info('Ivan', 25) display()
python decorators.py
wrapper executed this before display_info display_info ran with arguments (Ivan, 25) wrapper executed this before display display function ran
Вернёмся к декорированию функции, которая возвращала строку с умлаутом.
Более универасальный вариант декоратора заранее предусматривает передачу аргументов сможет справиться
и со старой версией функции, которая не принимала аргументы и с новой.
def escape_unicode(f): def wrap(*args): x = f(*args) return ascii(x) return wrap @escape_unicode def northen_city(): return "Tromsø" print(northen_city()) @escape_unicode def northen_city(city): return 'Northen city ' + city print(northen_city("Malmö"))
'Troms\xf8' 'Northen city Malm\xf6'
Обычно сразу же берут во внимание и *args и *kwargs
# In this example, the callable we # return is the local function wrap() # wrap() uses a closure to access f # after escape_unicode() returns def escape_unicode(f): def wrap(*args, **kwargs): x = f(*args, **kwargs) return ascii(x) return wrap # without decorator def northen_city(): return 'Tromsø' print(northen_city()) # with decorator @escape_unicode def northen_city(): return 'Tromsø' print(northen_city())
python escape_unicode.py
Tromsø 'Troms\xf8'
Ведение лога
В качестве примера использования декораторов можно привести запись лога о вызовах функций.
Декоратор создается один раз и потом его просто нужно добавлять к функциям, логи от которых нужно собрать. Это удобнее
чем добавлять код в каждую функцию отдельно.
def my_logger(orig_func): import logging logging.basicConfig(filename=f'{orig_func.__name__}.log', level=logging.INFO) def wrapper(*args, **kwargs): logging.info( f'Ran with args: {args} and kwargs: {kwargs}') return orig_func(*args, **kwargs) return wrapper @my_logger def display_info(name, age): print(f'display_info ran with arguments ({name}, {age})') display_info('Yuri', 27)
python decorators.py
display_info ran with arguments (Yuri, 27)
cat display_info.log
INFO:root:Ran with args: ('Yuri', 27) and kwargs: {}
Таймер
Ещё один похожий пример - таймер
def my_timer(orig_func): import time def wrapper(*args, **kwargs): t1 = time.time() result = orig_func(*args, **kwargs) t2 = time.time() - t1 print(f'{orig_func.__name__} ran in: {t2} sec') return result return wrapper import time @my_timer def display_info(name, age): time.sleep(2) print(f'display_info ran with arguments ({name}, {age})') display_info('Yuri', 27)
python decorators.py
display_info ran with arguments (Yuri, 27)
display_info ran in: 2.0023863315582275 sec
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги
Класс как декоратор
Классы, как и функции, это вызываемые объекты, поэтому могут использоваться как декораторы.
Функции, декорированные классом, заменяются на instance этого класса, которые должны быть также
вызываемыми. Поэтому декорировать классом можно только если у экземпляра объекта класса реализован метод __call__()
class decorator_class(object): def __init__(self, original_function): self.original_function = original_function def __call__(self, *args, **kwargs): print('call method executed this before {}'.format(self.original_function.__name__)) return self.original_function(*args, **kwargs) @decorator_class def display(): print('display function ran') @decorator_class def display_info(name, age): print('display_info ran with arguments ({}, {})'.format(name, age)) display_info('Ivan', 25) display()
python decorators.py
call method executed this before display_info display_info ran with arguments (Ivan, 25) call method executed this before display display function ran
Пример класса декоратора счётчика вызова функции
class CallCount: def __init__(self, f): self.f = f self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 return self.f(*args, **kwargs) @CallCount def hello(name): print(f'Hello, {name}') hello('Yuri') hello('Gherman') hello('Andiyan') hello('Pavel') print(hello.count)
Hello, Yuri Hello, Gherman Hello, Andiyan Hello, Pavel 4
Такой счетчик можно использовать, чтобы задать общий для всего рантайма момент времени. При первом вызове с помощью time получается текущий таймстамп затем он уже не обновляется до следующего запуска кода.
import time class TimeStampLock: """The idea is to generate timestamp once and then use it everywhere during runtime. Next run - new timestamp""" def __init__(self, f): self.f = f self.count = 0 if self.count == 0: self.timestr = time.strftime("%Y%m%d%H%M%S") else: self.timestr = self.timestr def __call__(self): if self.count == 0: print(time.strftime("%Y%m%d%H%M%S"), "self.count:", self.count, "- Creating new timestamp") else: print(time.strftime("%Y%m%d%H%M%S"), "self.count:", self.count, "- Reusing existing timestamp") self.count += 1 return self.timestr @TimeStampLock def get_timestamp(): pass if __name__ == '__main__': print(get_timestamp()) time.sleep(1) print(get_timestamp()) time.sleep(1) print(get_timestamp())
РЕКЛАМА хостинга Beget, которым я пользуюсь более десяти лет
Конец рекламы хостинга Beget, который я всем рекомендую.
Экземпляр объекта класса как декоратор
Декоратором может быть не сам класс а какой-то конкретный экземпляр объекта класса (instance)
class Trace: def __init__(self): self.enabled = True def __call__(self, f): def wrap(*args, **kwargs): if self.enabled: print(f'Calling {f}') return f(*args, **kwargs) return wrap tracer = Trace() @tracer def rotate_list(l): return l[1:] + [l[0]] l = [1, 2, 3] l = rotate_list(l) print(l) l = ["Fuengirola", "Barcelona", "Torremolinos"] l = rotate_list(l) print(l) tracer.enabled = False l = [4, 5, 6] l = rotate_list(l) print(l)
python class_instance_as_decorator.py
Calling <function rotate_list at 0x7fde19aeb040> [2, 3, 1] Calling <function rotate_list at 0x7fde19aeb040> ['Barcelona', 'Torremolinos', 'Fuengirola'] [5, 6, 4]
Несколько декораторов одновременно
Использование декораторов не ограничено одним декоратором на функцию.
Пример использования сразу трёх декораторов:
@decorator1 @decorator2 @decorator3 def my_function():
Порядок выполнения - снизу вверх
def escape_unicode(f): def wrap(*args, **kwargs): x = f(*args, **kwargs) return ascii(x) return wrap class Trace: def __init__(self): self.enabled = True def __call__(self, f): def wrap(*args, **kwargs): if self.enabled: print(f'Calling {f}') return f(*args, **kwargs) return wrap tracer = Trace() @tracer @escape_unicode def norwegian_island_maker(name): return name + 'øy' i = norwegian_island_maker('Java') print(i) i = norwegian_island_maker('Jakarta') print(i) tracer.enabled = False i = norwegian_island_maker('Cyprus') print(i) i = norwegian_island_maker('Сrete') print(i)
python multiple_decorators.py
Calling <function escape_unicode.<locals>.wrap at 0x7f1a49310280> 'Java\xf8y' Calling <function escape_unicode.<locals>.wrap at 0x7f1a49310280> 'Jakarta\xf8y' 'Cyprus\xf8y' 'Crete\xf8y'
Если просто использовать два декоратора подряд - тот что сверху получит не саму функцию, а то, что вернет нижний декоратор. Этого можно избежать использую functools.wraps()
from functools import wraps def my_logger(orig_func): import logging logging.basicConfig(filename=f'{orig_func.__name__}.log', level=logging.INFO) @wraps(orig_func) def wrapper(*args, **kwargs): logging.info( f'Ran with args: {args} and kwargs: {kwargs}') return orig_func(*args, **kwargs) return wrapper def my_timer(orig_func): import time @wraps(orig_func) def wrapper(*args, **kwargs): t1 = time.time() result = orig_func(*args, **kwargs) t2 = time.time() - t1 print(f'{orig_func.__name__} ran in: {t2} sec') return result return wrapper import time @my_timer @my_logger def display_info(name, age): time.sleep(2) print(f'display_info ran with arguments ({name}, {age})') display_info('Yuri', 27)
python decorators.py
display_info ran with arguments (Yuri, 27) display_info ran in: 2.0019609928131104 sec
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги
Декоратор для метода
Декораторы можно использовать не только с обычными функциями, но и с методами.
class Trace: def __init__(self): self.enabled = True def __call__(self, f): def wrap(*args, **kwargs): if self.enabled: print(f'Calling {f}') return f(*args, **kwargs) return wrap tracer = Trace() class IslandMaker: def __init__(self, suffix): self.suffix = suffix @tracer def make_island(self, name): return name + self.suffix im = IslandMaker(' Island') p = im.make_island('Python') print(p) c = im.make_island('C++') print(c)
python decorator_for_method.py
Calling <function IslandMaker.make_island at 0x7ff0ab2e5280> Python Island Calling <function IslandMaker.make_island at 0x7ff0ab2e5280> C++ Island
Потеря метаданных
Рассмотрим вызов простейшей функции с декоратором и без
>>> def hello(): ... "Print a well-known message." ... print('Hello, world!') ... >>> hello.__name__ 'hello' >>> hello.__doc__ 'Print a well-known message.' >>> help(hello) Help on function hello in module __main__: hello() Print a well-known message. (END)
Теперь то же самое но с декоратором, который ничего не делает
def noop(f): def noop_wrapper(): return f() return noop_wrapper @noop def hello(): "Print a well-known message." print("Hello, world!") help(hello) print(hello.__name__) print(hello.__doc__)
Help on function noop_wrapper in module __main__: noop_wrapper() (END) noop_wrapper None
Сохранить метаданные можно вручную записав их в декораторе
def noop(f): def noop_wrapper(): return f() noop_wrapper.__name__ = f.__name__ noop_wrapper.__doc__ = f.__doc__ return noop_wrapper @noop def hello(): "Print a well-known message." print("Hello, world!") help(hello)
Help on function hello in module __main__: hello() Print a well-known message. (END)
Более изящным решением является использование уже знакомого нам functools.wraps()
import functools def noop(f): @functools.wraps(f) def noop_wrapper(): return f() return noop_wrapper @noop def hello(): "Print a well-known message." print("Hello, world!") help(hello) print(hello.__name__) print(hello.__doc__)
Help on function hello in module __main__: hello() Print a well-known message. (END) hello Print a well-known message.
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги
Декоратор с параметрами
В декораторы можно передавать аргументы. Если вы пользовались Flask то видели как в декораторы передаются url @app.route("/") или @app.route("/about")
Рассмотрим уже знакомый пример:
def my_decorator(original_function): def wrapper_my_decorator(*args, **kwargs): print('wrapper executed this before {}'.format(original_function.__name__)) result = original_function(*args, **kwargs) print('Executed After', original_function.__name__, '\n') return result return wrapper_my_decorator @my_decorator def display_info(name, age): print(f'display_info ran with arguments ({name}, {age})') display_info('Ivan', 25) display_info('Yuri', 27)
python decorators_with_args.py
wrapper executed this before display_info display_info ran with arguments (Ivan, 25) Executed After display_info wrapper executed this before display_info display_info ran with arguments (Yuri, 27) Executed After display_info
Изменим его так, чтобы декоратор принимал аргументы
def prefix_decorator(prefix): def my_decorator(original_function): def wrapper_my_decorator(*args, **kwargs): print(prefix, 'wrapper executed this before {}'.format(original_function.__name__)) result = original_function(*args, **kwargs) print(prefix, 'Executed After', original_function.__name__, '\n') return result return wrapper_my_decorator return my_decorator @prefix_decorator('TESTING:') def display_info(name, age): print(f'display_info ran with arguments ({name}, {age})') display_info('Ivan', 25) display_info('Yuri', 27)
python decorators_with_args.py
TESTING: wrapper executed this before display_info display_info ran with arguments (Ivan, 25) TESTING: Executed After display_info TESTING: wrapper executed this before display_info display_info ran with arguments (Yuri, 27) TESTING: Executed After display_info
В следующем примере декорируем функцию, которая создаёт список. Декоратор будет принимать номер аргумента функции, который нужно проверить на неотрицательность.
def check_non_negative(index): def validator(original_function): def wrap(*args): if args[index] < 0: raise ValueError( f'Argument {index} must be non-negative') return original_function(*args) return wrap return validator # Проверим второй аргумент на неотрицательность # 0 это первый аргмент, значит передаём 1 @check_non_negative(1) def create_list(value, size): return [value] * size l = create_list('a', 3) print(l) m = create_list(123, -6) print(m)
['a', 'a', 'a'] Traceback (most recent call last): File "validating.py", line 20, in <module> m = create_list(123, -6) File "validating.py", line 5, in wrap raise ValueError( ValueError: Argument 1 must be non-negative
В примере выше check_non_negative() не является декоратором в том виде, в каком мы его определили.
Эта функция принимает не вызываемый объект (callable object) а число.
"Настоящим" декоратором является функция validator() именно она принимает декорируемую функцию как аргумент.
Любопытно выглядит запись такого декоратора без синтаксического сахара. Функция check_non_negative() остаётся
без изменений, только использовать её будем без @.
def check_non_negative(index): def validator(f): def wrap(*args): if args[index] < 0: raise ValueError( 'Argument {} must be non-negative.'.format(index)) return f(*args) return wrap return validator # Объявляем функцию не декорируя её @ def create_list(value, size): return [value] * size # "вручную" декорируем create_list() create_list = check_non_negative(1)(create_list) # Поведение остаётся таким же как и в прошлом примере # без ошибки print(create_list(hei, 2)) # выдаст ValueError print(create_list(1232, -3))
['hei', 'hei'] Traceback (most recent call last): File "check_non_negative.py", line 25, in <module> print(create_list(1232, -3)) File "check_non_negative.py", line 6, in wrap raise ValueError( ValueError: Argument 1 must be non-negative.
Функции | |
Функции первого класса | |
Python | |
Лямбда функции | |
map() | |
all() |
РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе
Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги