Декораторы 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  —  это мощный и элегантный способ изменения поведения функций или классов, не меняя исходный код. Декораторы способны: сократить код вдвое, улучшить его читаемость и повторное использование, разделить задачи и расширить функциональность имеющегося кода. 

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Ayush Thakur: Python Decorators That Can Reduce Your Code By Half

Предыдущая статьяЧто следует учитывать при разработке дизайна для людей с дислексией?
Следующая статьяReact-хуки useEffect и useLayoutEffect: различие и примеры использования