Декораторы в Python

Предупреждение
Последний раз данная статья обновлялась 05.05.2022, информация может быть устаревшей.

Список вопросов к Python собеседованию

Декоратор (Decorator) — структурный шаблон проектирования, предназначенный для динамического подключения дополнительного поведения к объекту. Шаблон Декоратор предоставляет гибкую альтернативу практике создания подклассов с целью расширения функциональности.1

Декоратор — это функция, которая позволяет обернуть другую функцию для расширения её функциональности без непосредственного изменения её кода. Python 2.4

Чтобы лучше пониманять работу декораторов нужно помнить тот факт, что:

  • в Python всё является объектами;
  • Функции — это объекты первого класса;
  • следовательно, язык поддерживает функции высших порядков.

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

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

Это означает, что мы можем:

  • сохранять функции в переменные;
  • передавать их в качестве аргументов;
  • возвращать из других функций;
  • определить одну функцию внутри другой (вложенные функции).

По сути, это и позволяет реализовать декоратор:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def decorator(func):
  print("Inside decorator")
  def wrapper():  # вложенная функция (обертка)
    print("Inside wrapper - Befor func exec")
    res = func()
    print("Inside wrapper - After func exec")
    return res
  print("Inside decorator 2")
  return wrapper  # возвращаем функцию (не вызывая)

def outer_func():
  print("Inside outer_func")

# Передаем функцию как параметр, результат присваиваем переменной
wrapped_func = decorator(outer_func)

# вызываем обернутую функцию первый раз
wrapped_func()
# вызываем обернутую функцию второй раз
wrapped_func()

Результат запуска:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# первый вызов функции wrapped_func()
Inside decorator - Befor wrapper def
Inside decorator - After wrapper def
Inside wrapper - Befor func exec
Inside outer_func
Inside wrapper - After func exec

# Второй вызов функции wrapped_func()
Inside wrapper - Befor func exec
Inside outer_func
Inside wrapper - After func exec

Выражение @decorator - это синтаксический сахар, короткая запись для outer_func = decorator(outer_func).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def decorator(func):
  def wrapper():
    func()
  return wrapper

@decorator
def outer_func():
  pass

outer_func()

Еще немного о декораторах:

  • декораторы можно вкладывать друг в друга (при этом порядок декорирования важен);
  • декораторы можно использовать с другими методами (например, «магическими»);
  • декораторы могут принимать в качестве аргументов не только функции.

Условные недостатки декораторов:

  • несколько замедляют вызов функции
  • если функция декорирована — это не отменить (существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика)
  • оборачивают функции, что может затруднить отладку (лечится использованием functools.wraps).

Области применения декораторов:

  • когда нужно избежать повторений при использовании похожих методов;
  • могут быть использованы для расширения возможностей функций из сторонних библиотек (код которых мы не можем изменять);
  • в Django декораторы используются для управления кешированием, контроля за правами доступа и определения обработчиков адресов;
  • в Twisted — для создания поддельных асинхронных inline-вызовов.

Допустим, теперь декорируемая функция outer_func() может принимать произвольное количество аргументов.

Чтобы наш декоратор работал корректно необходимо использовать *args и **kwargs (распаковка аргументов) во внутренней функции wrapper(), а так же передавать произвольное число позиционных и ключевых аргументов функции func(), которую декоратор получает в качестве аргумента:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def decorator(func):
  def wrapper(*args, **kwargs):
    res = func(*args, **kwargs)
    return res
  return wrapper

@decorator
def outer_func(a, b, name: str):
  pass

outer_func(7, 8, name="Tom")

Теперь декоратор @decorator будет работать как для функций, которые вообще не принимают аргументы, так и для функций которые принимают произвольное количество аргументов.

Из-за того, что декоратор возвращает не первоначальную функцию, а функцию обертку wrapper - теряется строка документации (docstring) основной функции, доступ к которой можно получить с помощью метода __doc__.

functools.wraps — декоратор Python 2.5

Функция wraps из модуля functools копирует всю информацию об оборачиваемой функции (имя, модуля, docstrings и т.п.) в функцию-обёртку.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from functools import wraps

def decorator(func):
    '''Декоратор'''
    @wraps(func)
    def wrapper():
        '''Функция wrapper'''
        func()
    return wrapper

@decorator
def outer_func():
    '''Оборачиваемая функция'''
    print('функция wrapped')

print(outer_func.__name__)
print(outer_func.__doc__)

Чтобы передать параметр в сам декоратор нужно добавить еще один слой абстракции, то есть — еще одну функцию-обертку.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from functools import wraps

def benchmark(type: str = "sec", iters: int = 1):  # новый уровень абстракции
  import time

  def decorator(func):  # сам декоратор
    @wraps(func)
    def wrapper(*args, **kwargs):  # функция обертка
      start_time = time.time()
      for _ in range(iters):
        result = func(*args, **kwargs)
      end_time = time.time()
      exec_time = end_time - start_time
      if type == "ms":
        print(f"Exec time of {func.__name__} x{iters} is {exec_time*1000} ms")
      else:
        print(f"Exec time of {func.__name__} x{iters} is {exec_time} s")
      return result

    return wrapper

  return decorator

@benchmark(type="ms", iters=100)
def outer_func(a, b, name: str):
  """Outer_func docstring"""
  print('функция wrapped')

Функция benchmark() не является декоратором. Это обычная функция, которая принимает аргументы type и iters, а затем возвращает декоратор. В свою очередь, он декорирует функцию outer_func(). Поэтому мы использовали не выражение @benchmark, а @benchmark(type="ms", iters=100) — круглые скобки означают, что функция вызывается, после чего возвращает сам декоратор.

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

Для функций с параметрами *args и **kwargs нужно передать в метод __call__.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Decorator:
  def __init__(self, func):
    print('Класс Decorator метод __init__')
    self.func = func

  def __call__(self, *args, **kwargs):
    print('перед вызовом класса...', self.func.__name__)
    self.func(*args, **kwargs)
    print('после вызова класса')

@Decorator
def outer_func(a, b, name: str):
  print('функция wrapped')

outer_func(7, 8, "Bob")

Для передачи аргументов в класс-декоратор эти аргументы получает инициализатор __init__. Метод __call__ будет получать декорируемую функцию и возвращать функцию-обертку, которая, по сути, будет выполнять эту декорируемую функцию. Функция-обертка wrapper получает *args и **kwargs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from functools import wraps

class Decorator:
  def __init__(self, output: str = "log"):
    print('Класс Decorator метод __init__')
    self.output = output

  def __call__(self, func):
    print('Класс Decorator метод __call__')

    @wraps(func)
    def wrapper(*args, **kwargs):
      print('перед вызовом func...', func.__name__)
      result = func(*args, **kwargs)
      if self.output == "file":
        print("Save to file")
      else:
        print("Console output")
      print('после вызова func')

      return result

    return wrapper

@Decorator(output="file")
def outer_func(a, b, name: str):
  """Outer_func docstring"""
  print('функция wrapped', a, b, name)

outer_func(7, 8, "Bob")

Функции и методы в Python — это практически одно и то же. Отличие в том, что методы всегда принимают первым параметром self (ссылку на объект). Следовательно, мы легко можем написать декоратор для метода:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def method_decorator(method):
  def wrapper(self):
    result = method(self)
    return result.upper() if self.power > 150 else result
  return wrapper

class Car:
  def __init__(self, power):
    self.power = power

  @method_decorator
  def wroom_wroom(self)
    return "wroom-wroom..."

lada = Car(87)
bmw = Car(300)
print(lada.wroom_wroom())  # wroom-wroom...
print(bmw.wroom_wroom())  # WROOM-WROOM...

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

Singleton — это класс с одним экземпляром. Его можно сохранить как атрибут функции-обертки и вернуть при запросе.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def singleton(cls):
  '''Класс Singleton (один экземпляр)'''
  def wrapper(*args, **kwargs):
    if not wrapper.instance:
      wrapper.instance = cls(*args, **kwargs)
    return wrapper.instance
  wrapper.instance = None
  return wrapper

@singleton
class SomeClass:
    pass
  • @classmethod
  • @staticmethod
  • @property

Подробнее в статье ООП в Python.

  • @contextlib.contextmanager
  • @functools.lru_cache
  • @abc.abstractmethod

Подробнее тут.2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import functools
def convert(func=None, convert_to=None):
    """Этот код конвертирует единицы измерения из одного типа в другой."""
    if func is None:
        return functools.partial(convert, convert_to=convert_to)
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Conversion unit: {convert_to}")
        val = func(*args, **kwargs)
        # Добавим правила для преобразования
        if convert_to is None:
            return val
        elif convert_to == "km":
            return val / 1000
        elif convert_to == "mile":
            return val * 0.000621371
        elif convert_to == "cm":
            return val * 100
        elif convert_to == "mm":
            return val * 1000
        else:
            raise ValueError("Conversion unit is not supported.") # этот тип единиц не поддерживается
    return wrapper
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import time

def exec_time(type: str = "sec", iters: int = 1):

    def exec_time_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """Docstring wrapper"""
            start_time = time.time()
            for _ in range(iters):
                result = func(*args, **kwargs)
            end_time = time.time()
            if type == "ms":
                print(f"Execution time of {func.__name__} x{iters} is {(end_time - start_time)*1000} ms")
            else:
                print(f"Execution time of {func.__name__} x{iters} is {end_time - start_time} s")
            return result

        return wrapper

    return exec_time_decorator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def counter(func):
    count = 0

    @wraps(func)
    def wrapper(*args, **kwargs):
        """Docstring wrapper"""
        nonlocal count
        count += 1
        res = func(*args, **kwargs)
        print(f"{func.__name__} была вызвана: {count}x")
        return res

    return wrapper

Источники3456