Декораторы в 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

РЕКЛАМА от Google. Может быть недоступна в вашем регионе.

Конец рекламы от Google. Если в блоке пусто считайте это рекламой моей телеги

Класс как декоратор

Классы, как и функции, это вызываемые объекты, поэтому могут использоваться как декораторы.

Функции, декорированные классом, заменяются на 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())

РЕКЛАМА от Яндекса. Может быть недоступна в вашем регионе

Конец рекламы от Яндекса. Если в блоке пусто считайте это рекламой моей телеги

Экземпляр объекта класса как декоратор

Декоратором может быть не сам класс а какой-то конкретный экземпляр объекта класса (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

РЕКЛАМА хостинга Beget, которым я пользуюсь более десяти лет

Изображение баннера

Конец рекламы хостинга Beget, который я всем рекомендую.

Декоратор для метода

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

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.

РЕКЛАМА от Google. Может быть недоступна в вашем регионе.

Конец рекламы от Google. Если в блоке пусто считайте это рекламой моей телеги

Декоратор с параметрами

В декораторы можно передавать аргументы. Если вы пользовались 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()

РЕКЛАМА хостинга Beget, которым я пользуюсь более десяти лет

Изображение баннера

Конец рекламы хостинга Beget, который я всем рекомендую.

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

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

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

@aofeed

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

@aofeedchat

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