Декораторы в Python

Содержание
Введение
Пример
Декорирование функции без параметров
Декорирование функции с параметрами
Логгер
Таймер
Класс как декоратор
Экземпляр объекта класса как декоратор
Несколько декораторов одновременно
Декораторы с параметрами
Похожие статьи

Введение

Нужно предварительно изучить темы функции первого класса и замыкания

Декораторы функций — вызываемые объекты, которые принимают другую функцию в качестве аргумента.

Декораторы функций могут производить операции с функцией и возвращают либо саму функцию, либо другую заменяющую её функцию или вызываемый объект.

То есть, если в коде ранее был прописан декоратор, названный my_decorator, то следующий код

@my_decorator def my_func():

Означает, что функция обёрнута в декоратор.

Первым делом Python обрабатывает функцию, которая завёрнута в декоратор. Получается объект функции.

Этот объект передаётся в функцию декоратор.

Декоратор возвращает изменённый объект функции обратно. Происходит новая связь между именем функции и объектом. То есть теперь функция my_func будет называться по-прежнему my_func но работать в соответствии с изменениями, внесёнными декторатором.

Начиная с версии Python 3.9 декоратором может быть любое валидное выражение. Подробнее в PEP 614

Пример

Создайте файл 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()

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: