12 декораторов Python, которые выведут ваш код на новый уровень
Декораторы Python – это мощные инструменты, которые помогают вам создавать чистый, эффективный и легко поддерживаемый код.
Я пишу эту статью как практическое руководство, чтобы помочь вам понять концепции, лежащие в основе этих объектов.
Эта статья представляет собой документированный список из 12 полезных декораторов, которые я регулярно использую в своих проектах, чтобы расширить свой код дополнительными функциональными возможностями.
Мы углубимся в каждый декоратор, посмотрим на код и поэкспериментируем с некоторыми практическими примерами.
Если вы разработчик на Python, этот пост расширит ваш инструментарий полезными скриптами и поможет повысить производительность и избежать дублирования кода.
1 — @logger (простой декоратор для начинающих)
Если вы новичок в декораторах, вы можете думать о них как о функциях, которые принимают другие функции в качестве входных данных и расширяют их возможности без изменения их основного назначения.
Давайте начнём с простого декоратора, который расширяет функцию, регистрируя время её запуска и окончания выполнения.
Результат оформления функции будет выглядеть следующим образом:
some_function(args)
# ----- some_function: start -----
# some_function executing
# ----- some_function: end -----
Чтобы написать этот декоратор, вам сначала нужно выбрать подходящее имя: давайте назовём его logger.
Logger– это функция, которая принимает другую функцию в качестве входных данных и возвращает функцию в качестве выходных данных. Функция вывода обычно представляет собой расширенную версию функции ввода. В нашем случае мы хотим, чтобы функция вывода поддерживала вызов функции ввода операторами start
и end
.
Поскольку мы не знаем, какие аргументы использует функция ввода, мы можем передать их из функции-оболочки, используя *args и **kwargs. Эти выражения позволяют передавать произвольное количество позиционных аргументов и аргументов ключевого слова.
Вот простая реализация декоратора logger:
def logger(function):
def wrapper(*args, **kwargs):
print(f"----- {function.__name__}: start -----")
output = function(*args, **kwargs)
print(f"----- {function.__name__}: end -----")
return output
return wrapper
Теперь вы можете применить logger к some_function
или любой другой функции, если уж на то пошло.
decorated_function = logger(some_function)
Python предоставляет для этого уникальный синтаксис, используя символ @.
@logger
def some_function(text):
print(text)
some_function("first test")
# ----- some_function: start -----
# first test
# ----- some_function: end -----
some_function("second test")
# ----- some_function: start -----
# second test
# ----- some_function: end -----
2 — @wraps
Этот декоратор обновляет функцию wrapper, чтобы она выглядела как исходная функция и наследовала её имя и свойства.
Чтобы понять, что делает @wraps и почему вы должны его использовать, давайте возьмём предыдущий декоратор и применим его к простой функции, которая добавляет два числа.
(Здесь ещё не используется @wraps)
def logger(function):
def wrapper(*args, **kwargs):
"""wrapper documentation"""
print(f"----- {function.__name__}: start -----")
output = function(*args, **kwargs)
print(f"----- {function.__name__}: end -----")
return output
return wrapper
@logger
def add_two_numbers(a, b):
"""this function adds two numbers"""
return a + b
Если мы проверим имя и документацию оформленной функции add_two_numbers
, вызвав атрибуты __name__
и __doc__
, мы получим неестественные (и всё же ожидаемые) результаты:
add_two_numbers.__name__
'wrapper'
add_two_numbers.__doc__
'wrapper documentation'
Это нежелательный результат. Мы хотим сохранить оригинальное название функции и документацию. Вот когда пригодится декоратор @wraps.
Всё, что вам нужно сделать – это использовать декоратор с функцией wrapper:
from functools import wraps
def logger(function):
@wraps(function)
def wrapper(*args, **kwargs):
"""wrapper documentation"""
print(f"----- {function.__name__}: start -----")
output = function(*args, **kwargs)
print(f"----- {function.__name__}: end -----")
return output
return wrapper
@logger
def add_two_numbers(a, b):
"""this function adds two numbers"""
return a + b
Перепроверив название и документацию, мы видим метаданные исходной функции (такой результат нам и требовался):
add_two_numbers.__name__
# 'add_two_numbers'
add_two_numbers.__doc__
# 'this function adds two numbers'
3 — @lru_cache
Это встроенный декоратор, который вы можете импортировать из functools
.
Он кэширует возвращаемые значения функции, используя алгоритмы кэширования (LRU) для удаления наименее используемых значений, когда кэш заполнен.
Обычно я использую этот декоратор для длительных задач, которые не изменяют выходные данные при одних и тех же входных данных, таких как запрос к базе данных, запрос статической удалённой веб-страницы или выполнение какой-либо интенсивной обработки.
В следующем примере я использую lru_cache
для оформления функции, которая имитирует некоторую обработку. Затем я применяю функцию к одному и тому же входному сигналу несколько раз подряд:
import random
import time
from functools import lru_cache
@lru_cache(maxsize=None)
def heavy_processing(n):
sleep_time = n + random.random()
time.sleep(sleep_time)
# first time
%%time
heavy_processing(0)
# CPU times: user 363 µs, sys: 727 µs, total: 1.09 ms
# Wall time: 694 ms
# second time
%%time
heavy_processing(0)
# CPU times: user 4 µs, sys: 0 ns, total: 4 µs
# Wall time: 8.11 µs
# third time
%%time
heavy_processing(0)
# CPU times: user 5 µs, sys: 1 µs, total: 6 µs
# Wall time: 7.15 µs
Если вы хотите самостоятельно реализовать декоратор кэша с нуля, вот как это можно сделать:
- Вы добавляете пустой словарь в качестве атрибута к функции wrapper для хранения ранее вычисленных функцией ввода значений
- При вызове функции ввода вы сначала проверяете, присутствуют ли её аргументы в кэше. Если это так, возвращайте результат. В противном случае вычислите его и поместите в кэш.
from functools import wraps
def cache(function):
@wraps(function)
def wrapper(*args, **kwargs):
cache_key = args + tuple(kwargs.items())
if cache_key in wrapper.cache:
output = wrapper.cache[cache_key]
else:
output = function(*args)
wrapper.cache[cache_key] = output
return output
wrapper.cache = dict()
return wrapper
@cache
def heavy_processing(n):
sleep_time = n + random.random()
time.sleep(sleep_time)
%%time
heavy_processing(1)
# CPU times: user 446 µs, sys: 864 µs, total: 1.31 ms
# Wall time: 1.06 s
%%time
heavy_processing(1)
# CPU times: user 11 µs, sys: 0 ns, total: 11 µs
# Wall time: 13.1 µs
4 — @repeat
Этот декоратор запускает вызов функции несколько раз подряд.
Это может быть полезно для целей отладки, стресс-тестирования или автоматизации повторения нескольких задач.
В отличие от предыдущих декораторов, этот ожидает ввода параметра.
def repeat(number_of_times):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(number_of_times):
func(*args, **kwargs)
return wrapper
return decorate
Следующий пример определяет декоратор с именем repeat
, который принимает некоторое количество повторений в качестве аргумента. Затем декоратор определяет функцию, называемую wrapper, которая оборачивается вокруг оформляемой функции. Функция wrapper вызывает оформленную функцию количество раз, равное указанному в аргументе числу:
@repeat(5)
def dummy():
print("hello")
dummy()
# hello
# hello
# hello
# hello
# hello
5 — @timeit
Этот декоратор измеряет время выполнения функции и выводит результат: он служит для отладки или мониторинга.
В следующем фрагменте декоратор timeit
измеряет время, необходимое для выполнения функции process_data
, и выводит его в секундах:
import time
from functools import wraps
def timeit(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f'{func.__name__} took {end - start:.6f} seconds to complete')
return result
return wrapper
@timeit
def process_data():
time.sleep(1)
process_data()
# process_data took 1.000012 seconds to complete
6 — @retry
Этот декоратор заставляет функцию повторять попытку несколько раз, когда она сталкивается с исключением.
Он принимает три аргумента: количество повторных попыток, исключение для перехвата и время ожидания между повторными попытками.
Это работает следующим образом:
- Функция wrapper запускает цикл for итераций
num_retries
. - На каждой итерации он вызывает функцию ввода в блоке try/except. Когда вызов выполняется успешно, он прерывает цикл и возвращает результат. В противном случае он переходит в спящий режим на секунды
sleep_time
и переходит к следующей итерации. - Когда вызов функции не выполняется успешно после завершения цикла for, функция wrapper вызывает исключение.
import random
import time
from functools import wraps
def retry(num_retries, exception_to_check, sleep_time=0):
"""
Decorator that retries the execution of a function if it raises a specific exception.
"""
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(1, num_retries+1):
try:
return func(*args, **kwargs)
except exception_to_check as e:
print(f"{func.__name__} raised {e.__class__.__name__}. Retrying...")
if i < num_retries:
time.sleep(sleep_time)
# Raise the exception if the function was not successful after the specified number of retries
raise e
return wrapper
return decorate
@retry(num_retries=3, exception_to_check=ValueError, sleep_time=1)
def random_value():
value = random.randint(1, 5)
if value == 3:
raise ValueError("Value cannot be 3")
return value
random_value()
# random_value raised ValueError. Retrying...
# 1
random_value()
# 5
7 — @countcall
Этот декоратор подсчитывает, сколько раз была вызвана функция.
Число, являющееся результатом, сохраняется в атрибуте count
.
from functools import wraps
def countcall(func):
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.count += 1
result = func(*args, **kwargs)
print(f'{func.__name__} has been called {wrapper.count} times')
return result
wrapper.count = 0
return wrapper
@countcall
def process_data():
pass
process_data()
process_data has been called 1 times
process_data()
process_data has been called 2 times
process_data()
process_data has been called 3 times
8 — @rate_limited
Это декоратор, который ограничивает скорость, с которой функция может быть вызвана, путём перехода в режим ожидания, если функция вызывается слишком часто.
import time
from functools import wraps
def rate_limited(max_per_second):
min_interval = 1.0 / float(max_per_second)
def decorate(func):
last_time_called = [0.0]
@wraps(func)
def rate_limited_function(*args, **kargs):
elapsed = time.perf_counter() - last_time_called[0]
left_to_wait = min_interval - elapsed
if left_to_wait > 0:
time.sleep(left_to_wait)
ret = func(*args, **kargs)
last_time_called[0] = time.perf_counter()
return ret
return rate_limited_function
return decorate
Декоратор работает, измеряя время, прошедшее с момента последнего вызова функции, и ожидая соответствующего промежутка времени, если это необходимо, чтобы убедиться, что ограничение скорости не превышено. Время ожидания вычисляется как min_interval - elapsed
, где min_interval
– минимальный интервал времени (в секундах) между двумя вызовами функции, а elapsed
– время, прошедшее с момента последнего вызова.
Если прошедшее время меньше минимального интервала, функция ожидает left_to_wait
секунд перед повторным выполнением.
Следовательно, эта функция приводит к небольшим временным затратам между вызовами, но гарантирует, что ограничение скорости не будет превышено.
pip install ratelimit
Чтобы использовать этот пакет, просто оформите какую-либо функцию, которая выполняет вызов API:
from ratelimit import limits
import requests
FIFTEEN_MINUTES = 900
@limits(calls=15, period=FIFTEEN_MINUTES)
def call_api(url):
response = requests.get(url)
if response.status_code != 200:
raise Exception('API response: {}'.format(response.status_code))
return response
Если оформленная функция вызывается больше раз, чем разрешено, возникает исключение ratelimit.RateLimitException
.
Чтобы иметь возможность обрабатывать это исключение, вы можете использовать декоратор sleep_and_retry
в сочетании с декоратором theratelimit
.
@sleep_and_retry
@limits(calls=15, period=FIFTEEN_MINUTES)
def call_api(url):
response = requests.get(url)
if response.status_code != 200:
raise Exception('API response: {}'.format(response.status_code))
return response
Это приводит к тому, что функция переходит в спящий режим оставшееся количество времени, прежде чем будет выполнена снова.
9 — @dataclass
Декоратор @dataclass
в Python используется для оформления классов.
Он автоматически генерирует специальные методы, такие как __init__, __repr__, __eq__, __lt__
и __str__
для классов, которые хранят данные. Это может сократить объём шаблонного кода и сделать классы более удобочитаемыми и ремонтопригодными.
Он также предоставляет готовые методы для красивого представления объектов, преобразования их в формат JSON, придания им неизменяемости и т.д.
Декоратор @dataclass
был представлен в Python 3.7 и доступен в стандартной библиотеке.
from dataclasses import dataclass,
@dataclass
class Person:
first_name: str
last_name: str
age: int
job: str
def __eq__(self, other):
if isinstance(other, Person):
return self.age == other.age
return NotImplemented
def __lt__(self, other):
if isinstance(other, Person):
return self.age < other.age
return NotImplemented
john = Person(first_name="John",
last_name="Doe",
age=30,
job="doctor",)
anne = Person(first_name="Anne",
last_name="Smith",
age=40,
job="software engineer",)
print(john == anne)
# False
print(anne > john)
# True
asdict(anne)
#{'first_name': 'Anne',
# 'last_name': 'Smith',
# 'age': 40,
# 'job': 'software engineer'}
10 — @register
Если ваш скрипт на Python случайно завершается, а вы всё ещё хотите выполнить некоторые задачи, чтобы сохранить свою работу, выполнить очистку или вывести сообщение, я нахожу, что декоратор @register будет довольно удобен в использовании.
from atexit import register
@register
def terminate():
perform_some_cleanup()
print("Goodbye!")
while True:
print("Hello")
При запуске этого скрипта и нажатии CTRL+C, мы видим вывод функцию terminate
.
11 — @property
Декоратор property используется для определения свойств класса, которые являются методами getter, setter и deleter атрибута экземпляра класса.
Используя этот декоратор, вы можете определить метод как класс property и получить к нему доступ, как если бы это был атрибут класса, без явного вызова метода.
Это полезно, если вы хотите добавить некоторые ограничения и логику проверки вокруг получения и установки значения.
В следующем примере мы определяем параметр для свойства rating, чтобы применить ограничение к входным данным (от 0 до 5):
class Movie:
def __init__(self, r):
self._rating = r
@property
def rating(self):
return self._rating
@rating.setter
def rating(self, r):
if 0 <= r <= 5:
self._rating = r
else:
raise ValueError("The movie rating must be between 0 and 5!")
batman = Movie(2.5)
batman.rating
# 2.5
batman.rating = 4
batman.rating
# 4
batman.rating = 10
# ---------------------------------------------------------------------------
# ValueError Traceback (most recent call last)
# Input In [16], in <cell line: 1>()
# ----> 1 batman.rating = 10
# Input In [11], in Movie.rating(self, r)
# 12 self._rating = r
# 13 else:
# ---> 14 raise ValueError("The movie rating must be between 0 and 5!")
#
# ValueError: The movie rating must be between 0 and 5!
12 — @singledispatch
Этот декоратор позволяет функции иметь разные реализации для разных типов аргументов.
from functools import singledispatch
@singledispatch
def fun(arg):
print("Called with a single argument")
@fun.register(int)
def _(arg):
print("Called with an integer")
@fun.register(list)
def _(arg):
print("Called with a list")
fun(1) # Prints "Called with an integer"
fun([1, 2, 3]) # Prints "Called with a list"
Заключение
Декораторы – это полезные абстракции для расширения вашего кода дополнительными функциями, такими как кэширование, автоматическое повторение попыток, ограничение скорости, логирование или превращение ваших классов в контейнеры данных.
Однако на этом дело не заканчивается, поскольку вы можете проявить больше креативности и внедрить свои собственные декораторы для решения очень специфических задач.
Спасибо за чтение!
Ресурсы
- https://medium.com/techtofreedom/9-python-built-in-decorators-that-optimize-your-code-significantly-bc3f661e9017
- https://towardsdatascience.com/10-of-my-favorite-python-decorators-9f05c72d9e33
- https://realpython.com/primer-on-python-decorators/#more-real-world-examples