Декораторы Python позволяют изменять поведение функции или класса, не меняя при этом исходный код. Это функции, принимающие другую функцию в качестве аргумента и возвращающие новую функцию, которая оборачивает (wrap) исходную. Таким образом, не меняя код исходной функции, можно добавлять к ней определенную дополнительную функциональность или логику.
Например, есть функция вывода сообщения на консоль:
def hello():
print("Hello, world!")
Предположим, нужно измерить длительность исполнения этой функции. Для этого можно написать другую функцию с модулем time
, вычисляющую время выполнения, а затем вызывающую исходную функцию:
import time
def measure_time(func):
def wrapper():
start = time.time()
func()
end = time.time()
print(f"Execution time: {end - start} seconds")
return wrapper
Функция measure_time
возвращает другую вызванную функцию wrapper
, которая является модифицированной версией исходной функции и выполняет две задачи: записывает времена начала и окончания выполнения, вызывает исходную функцию.
Теперь можно использовать эту функцию так:
hello = measure_time(hello)
hello()
Получим:
Hello, world!
Execution time: 0.000123456789 seconds
Не изменяя код функции hello
мы успешно добавили в нее небольшую дополнительную функциональность. Но сделать это можно и более элегантным, лаконичным способом. Декораторы — это простой и удобный синтаксический прием, позволяющий применять одну функцию к другой, используя символ @
. Например, предыдущий код можно переписать так:
@measure_time
def hello():
print("Hello, world!")
hello()
Результат тот же, но код гораздо более компактный. Строка @measure_time
равноценна hello = measure_time(hello)
, но выглядит намного чище и проще.
Использование декораторов Python
Декораторы Python полезны во многих случаях. Вот несколько примеров.
- Повторное (без повторений) использование кода. Например, есть несколько функций, для которых нужно измерять время их выполнения. Чтобы повторно не переписывать весь код, можно просто применить ко всем функциям один и тот же декоратор.
- Разделение задачи и следование принципу единой ответственности. Например, есть функция сложного вычисления. Декораторы можно использовать для ведения журнала, обработки ошибок, кэширования или проверки входных и выходных данных, не загромождая основную логику функции.
- Расширение функциональности имеющихся функций или классов, не меняя исходный код. Например, вы используете стороннюю библиотеку с некоторыми полезными функциями или классами. Когда нужно расширить их функциональность, можно использовать декораторы, чтобы обернуть и настроить функции или классы в соответствии с возникшими требованиями.
Примеры декораторов Python
В Python есть много встроенных декораторов, включая @staticmethod
, @classmethod
, @property
, @functools.lru_cache
, @functools.singledispatch
и др. Можно создавать под различные задачи и собственные декораторы. Рассмотрим несколько таких примеров для сокращения объема кода.
1. Декоратор @timer
Этот декоратор похож на уже рассмотренный @measure_time
, но его можно применить к любой функции, принимающей любое количество аргументов и возвращающей любое значение. При этом он использует декоратор functools.wraps
для сохранения имени и строки документации (docstring) исходной функции. Вот код:
import
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Execution time of {func.__name__}: {end - start} seconds")
return result
return wrapper
Используем этот декоратор для измерения времени выполнения любой функции, например:
@timer
def factorial(n):
"""Returns the factorial of n"""
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)
@timer
def fibonacci(n):
"""Returns the nth Fibonacci number"""
if n == 0 or n == 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(factorial(10))
print(fibonacci(10))
Результат может быть таким:
Execution time of factorial: 1.1920928955078125e-06 seconds
3628800
Execution time of fibonacci: 0.000123456789 seconds
55
2. Декоратор @debug
Этот декоратор полезен при отладке, поскольку он выводит (prints) имя, аргументы и возвращаемое значение оборачиваемой функции. Он также использует декоратор functools.wraps
для сохранения имени и docstring исходной функции. Вот код:
from functools import wraps
def debug(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args: {args} and kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
Используем этот декоратор для отладки любой функции, например:
@debug
def add(x, y):
"""Returns the sum of x and y"""
return x + y
@debug
def greet(name, message="Hello"):
"""Returns a greeting message with the name"""
return f"{message}, {name}!"
print(add(2, 3))
print(greet("Alice"))
print(greet("Bob", message="Hi"))
Результат может быть таким:
Calling add with args: (2, 3) and kwargs: {}
add returned: 5
5
Calling greet with args: ('Alice',) and kwargs: {}
greet returned: Hello, Alice!
Hello, Alice!
Calling greet with args: ('Bob',) and kwargs: {'message': 'Hi'}
greet returned: Hi, Bob!
Hi, Bob!
3. Декоратор @memoize
Этот декоратор полезен для оптимизации производительности рекурсивных или наиболее затратных функций, поскольку он кэширует результаты предыдущих вызовов и возвращает их, если те же аргументы передаются снова. Декоратор @memoize
также использует functools.wraps
для сохранения имени и docstring исходной функции. Вот код:
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper
Этот декоратор можно использовать для мемоизации (memoize) любой функции, например:
@memoize
def factorial(n):
"""Returns the factorial of n"""
if n == 0 or n == 1:
return 1
else:
return n * factorial(n - 1)
@memoize
def fibonacci(n):
"""Returns the nth Fibonacci number"""
if n == 0 or n == 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(factorial(10))
print(fibonacci(10))
На выходе будет то же самое значение, но время выполнения значительно сократится за счет кэширования и повторного использования результатов.
Заключение
Декораторы Python — это мощный и элегантный способ изменения поведения функций или классов, не меняя исходный код. Декораторы способны: сократить код вдвое, улучшить его читаемость и повторное использование, разделить задачи и расширить функциональность имеющегося кода.
Читайте также:
- Извлечение текста из PDF-файлов с помощью Python: исчерпывающее руководство
- Топ-13 пакетов Python в 2023 году
- Функциональные возможности Python, которые часто игнорируют
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ayush Thakur: Python Decorators That Can Reduce Your Code By Half