12 Декораторов Python, которые выведут ваш код на новый уровень

12 Декораторов Python, которые выведут ваш код на новый уровень

Декораторы Python - это мощные инструменты, которые помогают вам создавать чистый, многоразовый и поддерживаемый код.

Я долго ждал возможности узнать об этих абстракциях, и теперь, когда у меня появилось твёрдое представление, я пишу эту статью как практическое руководство, чтобы помочь вам тоже понять концепции, лежащие в основе этих объектов.

Эта статья скорее представляет собой документированный список из 12 полезных декораторов, которые я регулярно использую в своих проектах, чтобы расширить свой код дополнительными функциональными возможностями.Мы углубимся в каждый декоратор, посмотрим на код и поэкспериментируем с некоторыми практическими примерами.

Если вы Python-разработчик, эта статья расширит ваш инструментарий полезными скриптами, чтобы повысить производительность и избежать дублирования кода.

Меньше разговоров! Я предлагаю перейти к коду прямо сейчас 💻 .

1 — @logger✏

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

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

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

some_function(args) # ----- some_function: start ----- # some_function executing # ----- some_function: end -----

Чтобы написать этот декоратор, вам сначала нужно выбрать подходящее имя для него: давайте назовём его logger.

logger - это функция, которая принимает функцию в качестве входных данных и возвращает другую функцию в качестве выходных данных. Функция вывода обычно представляет собой расширенную версию функции ввода. В нашем случае мы хотим, чтобы функция вывода окружала вызов функции ввода операторами start и end.

Поскольку мы не знаем, какие аргументы использует функция ввода, мы можем передать их из функции wrapper, используя *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

Если бы вы захотели самостоятельно реализовать декоратор кэша с нуля, вот как это можно было бы сделать:

  • Вы добавляете пустой словарь в качестве атрибута к функции-оболочке для хранения ранее вычисленных значений функцией ввода
  • При вызове функции ввода вы сначала проверяете, присутствуют ли её аргументы в кэше. Если это так, верните результат. В противном случае вычислите его и поместите в кэш.
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 🔢

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

Это число сохраняется в атрибуте count .

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 left_to_wait = min_interval - elapsed if left_to_wait > 0: time.sleep(left_to_wait) ret = func(*args, **kargs) last_time_called = time.perf_counter() return ret return rate_limited_function return decorate

соответствующего промежутка времени, если это необходимо, чтобы убедиться, что ограничение скорости не превышено. Время ожидания вычисляется как min_interval - elapsed, где min_interval - минимальный интервал времени (в секундах) между двумя вызовами функции, а elapsed - время, прошедшее с момента последнего вызова.

Если прошедшее время меньше минимального интервала, функция ожидает left_to_wait секунд перед повторным выполнением.

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

Существует также сторонний пакет, который реализует ограничение скорости API: он называется ratelimit.

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 в сочетании с декоратором ratelimit.

@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,

12 Декораторов Python, которые выведут ваш код на новый уровень

мы видим вывод функции terminate.

11 — @property 🏠

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

Заключение

Декораторы - это полезные абстракции для расширения вашего кода дополнительными функциями, такими как кэширование, автоматическое повторение попыток, ограничение скорости, логирование или превращение ваших классов в контейнеры данных.

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

Вот список потрясающих декораторов, из которых можно черпать вдохновение.

Спасибо за чтение!

Ссылки на используемую литературу

Статья была взята из этого источника:

Начать дискуссию