Повысьте свои навыки программирования
12 декораторов Python, которые выведут ваш код на новый уровень
Делайте больше с меньшим количеством кода без ущерба для качества
Декораторы Python — это мощные инструменты, помогающие создавать чистый, пригодный для повторного использования и поддерживаемый код.
Я давно хотел узнать об этих абстракциях, и теперь, когда у меня есть четкое понимание, я пишу эту историю как практическое руководство, чтобы помочь вам понять концепции, лежащие в основе этих объектов.
Сегодня никаких громких вступлений и длинных теоретических определений.
Этот пост представляет собой задокументированный список из 12 полезных декораторов, которые я регулярно использую в своих проектах для расширения своего кода дополнительными функциями.
Мы углубимся в каждый декоратор, посмотрим на код и поэкспериментируем с ним. несколько практических примеров.
Если вы разработчик Python, этот пост расширит ваш набор инструментов полезными сценариями, чтобы повысить вашу производительность и избежать дублирования кода.
Меньше разговоров, предлагаю перейти к коду прямо сейчас 💻.
Новичок в Medium? Вы можете подписаться за 5 долларов в месяц и разблокировать неограниченное количество статей, которые я пишу о программировании, MLOps и проектировании систем, чтобы помочь специалистам по данным (или инженерам ML) создавать более качественный код.
Прежде чем приступить к чтению, имейте в виду, что лучше заменить операторы печати соответствующим регистратором. В следующих примерах я просто использовал отпечатки для простоты.
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
и почему вы должны его использовать, давайте возьмем предыдущий декоратор и применим его к простой функции, которая складывает два числа.
(Этот декоратор еще не использует @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декоратор.
Все, что вам нужно сделать, это украсить функцию-обертку.
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 — @повторить 🔁
Этот декоратор заставляет функцию вызываться несколько раз подряд.
Это может быть полезно для целей отладки, стресс-тестов или автоматизации повторения нескольких задач.
В отличие от предыдущих декораторов, этот ожидает входной параметр.
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 — @повторить 🔁
Этот декоратор заставляет функцию повторять несколько попыток, когда она сталкивается с исключением.
Он принимает три аргумента: количество повторных попыток, исключение для перехвата и повторной попытки и время ожидания между повторными попытками.
Это работает следующим образом:
- Функция-оболочка запускает цикл for из
num_retries
итераций. - На каждой итерации он вызывает функцию ввода в блоке try/except. Когда вызов успешен, он прерывает цикл и возвращает результат. В противном случае он приостанавливается на sleep_time секунд и переходит к следующей итерации.
- Если вызов функции не удался после завершения цикла for, функция-оболочка вызывает исключение.
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 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 — @регистрация 🛑
Если ваш скрипт Python случайно завершается, а вы все еще хотите выполнить некоторые задачи, чтобы сохранить свою работу, выполнить очистку или напечатать сообщение, я считаю, что в этом контексте очень удобен декоратор регистров.
from atexit import register @register def terminate(): perform_some_cleanup() print("Goodbye!") while True: print("Hello")
При запуске этого скрипта и нажатии CTRL+C,
мы видим вывод функции terminate
.
11 — @собственность 🏠
Декоратор свойств используется для определения свойств класса, которые по существу являются методами getter, setter и deleter для атрибута экземпляра класса.
Используя декоратор свойств, вы можете определить метод как свойство класса и получить к нему доступ, как если бы он был атрибутом класса, без явного вызова метода.
Это полезно, если вы хотите добавить некоторые ограничения и логику проверки для получения и установки значения.
В следующем примере мы определяем установщик для свойства рейтинга, чтобы применить ограничение на ввод (от 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