Декораторы в Python
Введение
Нужно предварительно изучить темы функции первого класса и замыкания
Декораторы функций — вызываемые объекты, которые принимают другую функцию в качестве аргумента.
Декораторы функций могут производить операции с функцией и возвращают либо саму функцию, либо другую заменяющую её функцию или вызываемый объект.
То есть, если в коде ранее был прописан декоратор, названный my_decorator, то следующий код
@my_decorator def my_func():
Означает, что функция обёрнута в декоратор.
Первым делом Python обрабатывает функцию, которая завёрнута в декоратор. Получается объект функции.
Этот объект передаётся в функцию декоратор.
Декоратор возвращает изменённый объект функции обратно. Происходит новая связь между именем функции
и объектом. То есть теперь функция my_func будет называться по-прежнему my_func но работать
в соответствии с изменениями, внесёнными декторатором.
Пример
Создайте файл
decorators.py
Внутри нужно создать функцию my_my_decorator() которая принимает в качестве аргумента
функцию, и функцию display(), которая пока не принимает никаких аргумнетов - её
мы будем декорировать.
Начнём с простого выполнения, но цель на будущее - изменять результат выполнения фукнции не изменяя кода функции.
def my_decorator(original_function): def wrapper(): return original_function() return wrapper def display(): print('display function ran') display = my_decorator(display) display()
python decorators.py
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 = my_decorator(display) display()
python decorators.py
wrapper executed this before display
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 @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)
Пример
# 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 display_info(name, age): print('display_info ran with arguments ({}, {})'.format(name, age)) display_info('Ivan', 25)
python decorators.py
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 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
Экземпляр объекта класса как декоратор
Декоратором может быть не сам класс а какой-то конкретный экземпляр объекта класса (instance)
classTrace: def__init__(self): self.enabled = True def__call__(self, f): defwrap(*args, **kwargs): ifself.enabled: print(f'Calling {f}') returnf(*args, **kwargs) returnwrap tracer = Trace() @tracer defrotate_list(l): returnl[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() |