Декораторы Python

PythonBeginner

Введение

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

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

Базовый декоратор (без излишеств)

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

Примечание: Если вы не знакомы с использованием WebIDE (похожего на VS Code), изучите его сначала в этой лаборатории.

Сначала откройте файл step1.py в проводнике файлов слева.

Теперь скопируйте следующий код в файл step1.py. Этот код определяет простой декоратор и применяет его к функции.

## Декоратор: функция, которая принимает другую функцию и возвращает обертку (wrapper)
def your_decorator(func):
  def wrapper():
    ## Делаем что-то до вызова func...
    print("Before func!")
    func()  ## Вызываем оригинальную функцию
    ## Делаем что-то после вызова func...
    print("After func!")
  return wrapper  ## Возвращаем функцию-обертку

## @your_decorator — это синтаксический сахар для: foo = your_decorator(foo)
@your_decorator
def foo():
  print("Hello World!")

foo()  ## Вызывает wrapper, который вызывает foo с дополнительным поведением

Давайте разберем код:

  • your_decorator(func): Это функция-декоратор. Она принимает один аргумент, func, — функцию, которую нужно декорировать.
  • wrapper(): Это внутренняя функция, которая "оборачивает" (wraps) оригинальную функцию. Она содержит новое поведение, которое вы хотите добавить. Здесь она выводит сообщения до и после вызова оригинальной функции.
  • @your_decorator: Это синтаксический сахар для применения декоратора. Он эквивалентен написанию foo = your_decorator(foo) после определения функции.

Теперь запустите скрипт в терминале, чтобы увидеть декоратор в действии.

python3 step1.py

Вы должны увидеть следующий вывод, который показывает, что функция wrapper была выполнена, добавив поведение вокруг оригинальной функции foo.

Before func!
Hello World!
After func!

Декоратор для функции с параметрами

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

Для этого вы можете использовать *args и **kwargs в сигнатуре функции-обёртки (wrapper). Это позволяет обёртке принимать любое количество позиционных и именованных аргументов и передавать их исходной функции.

Откройте файл step2.py и добавьте следующий код:

## Decorator that works with functions that have parameters
def your_decorator(func):
  def wrapper(*args,**kwargs):  ## Accept any arguments
    ## Do stuff before func...
    print("Before func!")
    func(*args,**kwargs)  ## Pass arguments to original function
    ## Do stuff after func...
    print("After func!")
  return wrapper

@your_decorator
def foo(bar):
  print("My name is " + bar)

foo("Jack")  ## Arguments are passed through wrapper

В этом коде функция wrapper определена с использованием *args и **kwargs. Когда вызывается foo("Jack"), аргумент "Jack" захватывается *args в wrapper и затем передается исходной функции func при выполнении func(*args, **kwargs).

Запустите скрипт, чтобы увидеть результат.

python3 step2.py

Вывод показывает, что аргумент был успешно передан через декоратор исходной функции.

Before func!
My name is Jack
After func!

Шаблон для базового декоратора

На этом шаге вы откроете для себя шаблон наилучшей практики для создания надежных декораторов. Предыдущие декораторы работают, но имеют побочный эффект: они теряют метаданные исходной функции (такие как ее имя __name__ и строка документации __doc__).

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

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

Откройте файл step3.py и добавьте следующий код.

import functools

## Best practice decorator template: preserves function metadata and return value
def your_decorator(func):
  @functools.wraps(func)  ## Preserves function name, docstring, etc.
  def wrapper(*args,**kwargs):
    print("Before function execution...")
    result = func(*args,**kwargs)  ## Call function and capture return value
    print("After function execution...")
    return result  ## Return the original function's return value
  return wrapper

@your_decorator
def add(a, b):
  """This function adds two numbers."""
  return a + b

sum_result = add(5, 3)
print(f"Function name: {add.__name__}")
print(f"Docstring: {add.__doc__}")
print(f"Result: {sum_result}")

Ключевые улучшения в этом шаблоне:

  • import functools: Импортирует необходимый модуль.
  • @functools.wraps(func): Это декоратор, применяемый к функции wrapper. Он копирует метаданные из исходной функции func (такие как __name__ и __doc__) в функцию wrapper.
  • result = func(*args, **kwargs): Возвращаемое значение исходной функции захватывается в переменной result.
  • return result: Функция-обёртка возвращает захваченный результат, чтобы декорированная функция вела себя ожидаемым образом.

Выполните скрипт:

python3 step3.py

Вывод демонстрирует, что имя и строка документации функции сохраняются, а возвращаемое значение обрабатывается корректно.

Before function execution...
After function execution...
Function name: add
Docstring: This function adds two numbers.
Result: 8

Декоратор с параметрами

На этом шаге вы узнаете, как создавать декораторы, которые можно настраивать с помощью собственных параметров. Это позволяет использовать декораторы более гибко и конфигурируемо, например, @your_decorator(arg='value').

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

Давайте создадим декоратор, который принимает строку prefix и выводит ее до и после вызова функции. Откройте step4.py и добавьте следующий код:

import functools

## Decorator factory: returns a decorator based on parameters
def prefix_decorator(prefix):
  def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
      ## Use the 'prefix' argument from the factory
      print(f"{prefix} Before function call")
      result = func(*args, **kwargs)
      print(f"{prefix} After function call")
      return result
    return wrapper
  return decorator  ## Return the actual decorator function

## Using the decorator with a parameter
@prefix_decorator(prefix="LOG:")
def say_hello(name):
  print(f"Hello, {name}!")

say_hello("Alice")

Вот как это работает:

  1. Сначала вызывается prefix_decorator(prefix="LOG:"). Это функция-фабрика.
  2. Она возвращает функцию decorator, которая теперь имеет доступ к переменной prefix ("LOG:") через замыкание (closure).
  3. Этот возвращенный decorator затем применяется к say_hello, декорируя ее.

Запустите скрипт, чтобы увидеть, как используется параметр.

python3 step4.py

Вывод показывает префикс "LOG:", который был передан в качестве аргумента декоратору.

LOG: Before function call
Hello, Alice!
LOG: After function call

Декораторы на основе классов

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

Откройте файл step5.py, чтобы начать.

Шаблон 1: Декорирование метода класса

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

Добавьте следующий код в step5.py. Этот пример определяет декоратор для обработки исключений в методе класса.

## Pattern 1: Decorating a method within a class
class DecorateMyMethod:

  ## Static method decorator for methods with only 'self' parameter
  def decorator_for_class_method_with_no_args(method):
    def wrapper_for_class_method(self):  ## Only takes self
      try:
        return method(self)  ## Call original method
      except Exception as e:
        print(f"\nWARNING: An exception occurred: {e}\n")
    return wrapper_for_class_method

  def __init__(self,succeed:bool):
    self.succeed = succeed

  @decorator_for_class_method_with_no_args
  def class_action(self):
    if self.succeed:
      print("You succeeded by choice.")
    else:
      raise Exception("Epic fail of your own creation.")

print("--- Testing Pattern 1 ---")
test_succeed = DecorateMyMethod(True)
test_succeed.class_action()

test_fail = DecorateMyMethod(False)
test_fail.class_action()
print("-" * 25)

Шаблон 2: Декоратор как класс

Более распространенным и мощным шаблоном является определение самого декоратора как класса. Это идеально подходит для случаев, когда декоратору необходимо поддерживать состояние. Реализуя методы __init__() и __call__(), вы можете заставить экземпляр класса вести себя как функция.

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

## Pattern 2: Class-based decorator for maintaining state
class CountCallNumber:

  def __init__(self, func):
    self.func = func  ## Store the function to decorate
    self.call_number = 0  ## Initialize call counter

  def __call__(self, *args, **kwargs):  ## Makes instance callable
    self.call_number += 1  ## Increment counter
    print("This is execution number " + str(self.call_number))
    return self.func(*args, **kwargs)  ## Call original function

@CountCallNumber
def say_hi(name):
  print("Hi! My name is " + name)

print("\n--- Testing Pattern 2 ---")
say_hi("Jack")
say_hi("James")
  • __init__(self, func): Это конструктор. Он вызывается, когда применяется декоратор (@CountCallNumber). Он получает декорируемую функцию (say_hi) и сохраняет ее.
  • __call__(self, *args, **kwargs): Этот метод делает экземпляр класса вызываемым. Когда вы вызываете декорированную функцию (say_hi("Jack")), вы на самом деле вызываете этот метод __call__. Он увеличивает счетчик, а затем вызывает исходную сохраненную функцию.

Теперь запустите весь скрипт.

python3 step5.py

Вы увидите вывод для обоих шаблонов. Обратите внимание, как увеличивается счетчик вызовов во второй части.

--- Testing Pattern 1 ---
You succeeded by choice.

WARNING: An exception occurred: Epic fail of your own creation.

-------------------------

--- Testing Pattern 2 ---
This is execution number 1
Hi! My name is Jack
This is execution number 2
Hi! My name is James

Резюме

Поздравляем с завершением лабораторной работы! Вы получили прочное понимание декораторов Python и их различных форм.

В этой лабораторной работе вы научились:

  • Создавать базовый декоратор и понимать синтаксис @.
  • Разрабатывать декораторы, которые работают с функциями, принимающими любое количество аргументов, используя *args и **kwargs.
  • Использовать шаблон наилучшей практики с functools.wraps для сохранения метаданных функции и обработки возвращаемых значений.
  • Создавать фабрики декораторов, которые позволяют настраивать ваши декораторы с помощью параметров.
  • Реализовывать декораторы в виде классов для поддержания состояния между вызовами функций, например, в счетчике вызовов.

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