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

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

Введение

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

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

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

То есть, если в коде ранее был прописан декоратор, названный 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.

Banner Image

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

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

Любопытно выглядит запись такого декоратора без синтаксического сахара.

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 # Create the function without decorating it def create_list(value, size): return [value] * size # "manually" decorate create_list() create_list = check_non_negative(1)(create_list) # It now behaves the same as if it were decorated normally print(create_list(1232, 4))

Related Articles
Функции
Функции первого класса
Python
Лямбда функции
map()
all()

Search on this site

Subscribe to @aofeed channel for updates

Visit Channel

@aofeed

Feedbak and Questions in Telegram

@aofeedchat