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

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

Введение

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

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

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

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

@my_decorator def my_func():

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

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

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

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

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

Пример

Создайте файл decorators.py

Внутри нужно создать функцию 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

Обратите внимание на то, что wrapper вызывает исходную функцию

def my_decorator(original_function): def wrapper(): return original_function() return wrapper

В этом примере когда мы делаем вызов decorated_display() происходит следующее:

decorated_display()wrapper()display() → print()

То есть мы просто вызвали display() через посредника. Далее, когда мы рассмотрим более сложные примеры, результат вызова декорированной функции будет отличаться от результат вызова оригинальной. В этом и состоит смысл декораторов.

Чтобы декоратор делал хотя бы что-то видимое добавим в него вывод текстового сообщения.

def my_decorator(original_function): def wrapper(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper def display(): print('display function ran') # Вызов с использованием декоратора decorated_display = my_decorator(display) decorated_display() # Простой вызов функции display()

python decorators.py

wrapper executed this before display display function ran display function ran

Теперь вызов с декоратором производит два принта. А вызов исходной функции один. Уже на таком простом примере, можно почувствовать, что декорирование это очень мощный инструмент.

Когда мы создали функцию decorated_display мы сохранили возможность вызвать обычный display

Если это не нужно, можно просто переопределить display

# Вызов с использованием декоратора display = my_decorator(display) display()

Запись декоратора через @

Допустим, что декоратор my_decorator() существует. Мы уже знаем один способ декорирования, но его довольно редко используют на практике.

Обычно функции декорируют с помощью @. Изучим этот способ и его отличия от предыдущего.

Следующие две записи идентичны по смыслу

# 1 @my_decorator def my_func(): print('Hello') # 2 def my_func(): print('Hello') my_func = my_decorator(my_func)

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

Оба способа занимают всего по одной строке. Применение @ над объявлением функции сразу сигнализирует о том, что функция задекорирована. Это удобно.

С другой стороны @ автоматически переопределяет исходную функцию, не оставляя нам выбора - создавать новую или переопределить старую. О том как сохранить доступ и изначальной функции читайте здесь.

Декорирование функции без параметров

Из предыдущей главы мы знаем, что следующие две записи идентичны по смыслу

# 1 @my_decorator def display(): print('display function ran') # 2 def display(): print('display function ran') display = my_decorator(display)

В дальнейшем будем использовать оформление декоратора через @. Тем не менее, для объяснения происходящего «под капотом» я всё ещё буду прибегать к записи без синтаксического сахара.

def my_decorator(original_function): def wrapper(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper @my_decorator def display(): print('display function ran') display()

python decorators.py

wrapper executed this before display
display function ran

Пример

Рассмотрим функцию, которая возвращает название города, в котором содержатся умлауты

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()

Про декорирование этой функции в случае когда она будет принимать аргумент вы можете прочитать здесь либо последовательно прочитать следующую главу - «Декорирование функции с параметрами»

Декорирование функции с параметрами

Рассмотрим функцию 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(): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function() return wrapper @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() takes 0 positional arguments but 2 were given

Чтобы разобраться в сути ошибки рассмотрим как бы это выглядело без @

Помним, что my_decorator возвращает функцию wrapper, которая аргументов не принимает.

Когда мы декорируем мы делаем следующее:

display_info = my_decorator(display_info)

То есть display_info это и есть wrapper, никаких аргументов он не ждёт. И тут мы вдруг вызываем

display_info('Ivan', 25)

Если решить эту проблему добавлением двух аргументов в функцию wrapper из декоратора

def my_decorator(original_function): def wrapper(name, age): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function(name, age) return wrapper … display()

То с display_info(name, age) он будет работать а с display() уже нет - эти аргументы лишние и функция их не ждёт.

В момент вызова display() обнаружится, что display это wrapper, который ждёт два аргумента, а их нет.

Traceback (most recent call last): File "C:\A\decorator.py", line 21, in <module> display() ~~~~~~~^^ TypeError: my_decorator.<locals>.wrapper() missing 2 required positional arguments: 'name' and 'age'

Можно продвинуться чуть дальше задав значения по умолчанию

def my_decorator(original_function): def wrapper(name="Andrei", age=7): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function(name, age) return wrapper

Теперь wrapper хоть и ждёт два аргумента, но может работать и без них.

В этом случае ошибка будет при попытке вызвать исходную функцию внутри обёртки.

Traceback (most recent call last): File "C:\A\decorator.py", line 21, in <module> display() ~~~~~~~^^ File "C:\A\decorator.py", line 4, in wrapper return original_function(name, age) TypeError: display() takes 0 positional arguments but 2 were given

original_function это display а он никаких аргументов не ждёт. Понятно, что если мы уберём аргументы оттуда - сломается вызов display_info

Сделать декоратор универсальным можно воспользовавшись *args, **kwargs

def my_decorator(original_function): def wrapper(*args, **kwargs): print('wrapper executed this before {}'.format(original_function.__name__)) return original_function(*args, **kwargs) return wrapper @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

Вернёмся к примеру с декорированием функции, которая возвращала строку с умлаутом.

Если бы функция 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

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

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

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

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

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

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

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

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

20250121141024 self.count: 0 - Creating new timestamp 20250121141024 20250121141025 self.count: 1 - Reusing existing timestamp 20250121141024 20250121141026 self.count: 2 - Reusing existing timestamp 20250121141024

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

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

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

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

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

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

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

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

В декораторы можно передавать аргументы. Если вы пользовались Flask то видели как в декораторы передаются url @app.route("/") или @app.route("/about")

Рассмотрим уже знакомый пример:

def my_decorator(original_function): def wrapper(*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 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(*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 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.

Сохранить изначальную функцию

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

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

Первый путь - не использовать @ а создать декорированную функцию с другим именем - как мы делали в этом примере

Второй путь - использовать @ и при этом явно создать ссылку на изначальную функцию.

# decorators.py def my_decorator(func): def wrapper(): print('wrapper executed this before {}'.format(func.__name__)) return func() wrapper._original = func return wrapper @my_decorator def f(): print('https://devhops.ru') # Вызов с использованием декоратора f() # Вызов изначальной функции f._original()

python decorators.py

wrapper executed this before f https://devhops.ru https://devhops.ru

Автор статьи: Андрей Олегович

Похожие статьи
Функции
Функции первого класса
Python
Лямбда функции
map()
all()

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

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

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

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

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

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

@aofeed

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

@aofeedchat

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