Цепочки декораторов и параметризованные декораторы

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

Вы научитесь объединять несколько декораторов, создавать декораторы, принимающие параметры, сохранять метаданные функций при использовании декораторов и применять декораторы к различным типам методов класса. Файлы, с которыми вы будете работать, это logcall.py, validate.py и sample.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/lambda_functions("Lambda Functions") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/class_static_methods("Class Methods and Static Methods") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/function_definition -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/lambda_functions -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/scope -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/classes_objects -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/class_static_methods -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} python/decorators -.-> lab-132515{{"Цепочки декораторов и параметризованные декораторы"}} end

Сохранение метаданных функций в декораторах

В Python декораторы - это мощный инструмент, который позволяет изменять поведение функций. Однако, когда вы используете декоратор для обертывания функции, возникает небольшая проблема. По умолчанию метаданные исходной функции, такие как ее имя, строка документации (docstring) и аннотации, теряются. Метаданные важны, так как они помогают в интроспекции (исследовании структуры кода) и создании документации. Давайте сначала проверим эту проблему.

Откройте терминал в WebIDE. Мы выполним несколько команд Python, чтобы увидеть, что происходит, когда мы используем декоратор. Следующие команды создадут простую функцию add, обернутую в декоратор, а затем напечатают функцию и ее строку документации.

cd ~/project
python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

Когда вы выполните эти команды, вы увидите вывод, похожий на следующий:

<function wrapper at 0x...>
None

Обратите внимание, что вместо названия функции add выводится wrapper. А строка документации, которая должна быть 'Adds two things', равна None. Это может стать большой проблемой, когда вы используете инструменты, которые зависят от этих метаданных, таких как инструменты интроспекции или генераторы документации.

Исправление проблемы с помощью functools.wraps

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

  1. Сначала откройте файл logcall.py в WebIDE. Вы можете перейти в директорию проекта, используя следующую команду в терминале:
cd ~/project
  1. Теперь обновите декоратор logged в файле logcall.py следующим кодом. Декоратор @wraps(func) здесь является ключевым. Он копирует все метаданные из исходной функции func в обертку (wrapper) функции.
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
  1. Декоратор @wraps(func) выполняет важную работу. Он берет все метаданные (например, имя, строку документации и аннотации) из исходной функции func и присоединяет их к функции-обертке wrapper. Таким образом, когда мы используем декорированную функцию, она будет иметь правильные метаданные.

  2. Давайте протестируем наш улучшенный декоратор. Выполните следующие команды в терминале:

python3 -c "from logcall import logged; @logged
def add(x,y):
    'Adds two things'
    return x+y
    
print(add)
print(add.__doc__)"

Теперь вы должны увидеть:

<function add at 0x...>
Adds two things

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

Исправление декоратора в validate.py

Теперь давайте применим то же исправление к декоратору validated в файле validate.py. Этот декоратор используется для проверки типов аргументов функции и возвращаемого значения на основе аннотаций функции.

  1. Откройте файл validate.py в WebIDE.

  2. Обновите декоратор validated с использованием декоратора @wraps. Следующий код показывает, как это сделать. Декоратор @wraps(func) добавляется к функции-обертке wrapper внутри декоратора validated для сохранения метаданных.

from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper
  1. Давайте проверим, что наш декоратор validated теперь сохраняет метаданные. Выполните следующие команды в терминале:
python3 -c "from validate import validated, Integer; @validated
def multiply(x: Integer, y: Integer) -> Integer:
    'Multiplies two integers'
    return x * y
    
print(multiply)
print(multiply.__doc__)"

Вы должны увидеть:

<function multiply at 0......>
Multiplies two integers

Теперь оба декоратора, logged и validated, правильно сохраняют метаданные функций, которые они декорируют. Это гарантирует, что при использовании этих декораторов функции будут по-прежнему иметь свои исходные имена, строки документации и аннотации, что очень полезно для читаемости и поддерживаемости кода.

✨ Проверить решение и практиковаться

Создание декораторов с аргументами

До сих пор мы использовали декоратор @logged, который всегда выводит фиксированное сообщение. Но что, если вы хотите настроить формат сообщения? В этом разделе мы научимся создавать новый декоратор, который может принимать аргументы, предоставляя вам больше гибкости в использовании декораторов.

Понимание параметризованных декораторов

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

def decorator_with_args(arg1, arg2, ...):
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Use arg1, arg2, ... here
            ## Call the original function
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

Когда вы используете @decorator_with_args(value1, value2) в своем коде, Python сначала вызывает decorator_with_args(value1, value2). Этот вызов возвращает фактический декоратор, который затем применяется к функции, следующей за синтаксисом @. Этот двухшаговый процесс является ключом к тому, как работают параметризованные декораторы.

Создание декоратора logformat

Давайте создадим декоратор @logformat(fmt), который принимает строку формата в качестве аргумента. Это позволит нам настроить сообщение логгирования.

  1. Откройте файл logcall.py в WebIDE и добавьте новый декоратор. Ниже приведен код, показывающий, как определить как существующий декоратор logged, так и новый декоратор logformat:
from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

В декораторе logformat внешняя функция logformat принимает строку формата fmt в качестве аргумента. Затем она возвращает функцию decorator, которая является фактическим декоратором, модифицирующим целевую функцию.

  1. Теперь давайте протестируем наш новый декоратор, изменив файл sample.py. Следующий код показывает, как использовать как декоратор logged, так и декоратор logformat для разных функций:
from logcall import logged, logformat

@logged
def add(x, y):
    "Adds two numbers"
    return x + y

@logged
def sub(x, y):
    "Subtracts y from x"
    return x - y

@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x, y):
    "Multiplies two numbers"
    return x * y

Здесь функции add и sub используют декоратор logged, в то время как функция mul использует декоратор logformat с пользовательской строкой формата.

  1. Запустите обновленный файл sample.py, чтобы увидеть результаты. Откройте терминал и выполните следующую команду:
cd ~/project
python3 -c "import sample; print(sample.add(2, 3)); print(sample.mul(2, 3))"

Вы должны увидеть вывод, похожий на следующий:

Calling add
5
sample.py:mul
6

Этот вывод показывает, что декоратор logged выводит имя функции, как и ожидалось, а декоратор logformat использует пользовательскую строку формата для вывода имени файла и имени функции.

Переопределение декоратора logged с использованием logformat

Теперь, когда у нас есть более гибкий декоратор logformat, мы можем переопределить наш исходный декоратор logged, используя его. Это поможет нам повторно использовать код и поддерживать единообразный формат логгирования.

  1. Обновите файл logcall.py следующим кодом:
from functools import wraps

def logformat(fmt):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(fmt.format(func=func))
            return func(*args, **kwargs)
        return wrapper
    return decorator

## Define logged using logformat
logged = lambda func: logformat("Calling {func.__name__}")(func)

Здесь мы используем лямбда-функцию для определения декоратора logged на основе декоратора logformat. Лямбда-функция принимает функцию func и применяет декоратор logformat с определенной строкой формата.

  1. Проверьте, что переопределенный декоратор logged по-прежнему работает. Откройте терминал и выполните следующую команду:
cd ~/project
python3 -c "from logcall import logged; @logged
def greet(name):
    return f'Hello, {name}'
    
print(greet('World'))"

Вы должны увидеть:

Calling greet
Hello, World

Это показывает, что переопределенный декоратор logged работает как ожидалось, и мы успешно повторно использовали декоратор logformat для достижения единообразного формата логгирования.

✨ Проверить решение и практиковаться

Применение декораторов к методам класса

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

Понимание проблемы

Давайте посмотрим, что происходит, когда мы применяем наш декоратор @logged к разным типам методов. Декоратор @logged, вероятно, используется для логирования информации о вызовах методов.

  1. Создайте новый файл methods.py в WebIDE. Этот файл будет содержать наш класс с разными типами методов, декорированными декоратором @logged.
from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @logged
    @classmethod
    def class_method(cls):
        print("Class method called")
        return "class result"

    @logged
    @staticmethod
    def static_method():
        print("Static method called")
        return "static result"

    @logged
    @property
    def property_method(self):
        print("Property method called")
        return "property result"

В этом коде у нас есть класс Spam с четырьмя разными типами методов. Каждый метод декорирован декоратором @logged, а некоторые также декорированы другими встроенными декораторами, такими как @classmethod, @staticmethod и @property.

  1. Давайте протестируем, как это работает. Мы выполним команду Python в терминале, чтобы вызвать эти методы и посмотреть на вывод.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

При выполнении этой команды вы, возможно, заметите некоторые проблемы:

  • Декоратор @property может не работать корректно с нашим декоратором @logged. Декоратор @property используется для определения метода как свойства, и он имеет особый способ работы. При использовании вместе с декоратором @logged могут возникнуть конфликты.
  • Порядок декораторов имеет значение для @classmethod и @staticmethod. Порядок применения декораторов может изменить поведение метода.

Порядок применения декораторов

Когда вы применяете несколько декораторов, они применяются снизу вверх. Это означает, что декоратор, ближайший к определению метода, применяется первым, а затем последовательно применяются те, которые находятся выше. Например:

@decorator1
@decorator2
def func():
    pass

Это эквивалентно:

func = decorator1(decorator2(func))

В этом примере decorator2 применяется к func первым, а затем decorator1 применяется к результату decorator2(func).

Исправление порядка декораторов

Давайте обновим наш файл methods.py, чтобы исправить порядок декораторов. Изменяя порядок декораторов, мы можем убедиться, что каждый метод работает как ожидается.

from logcall import logged

class Spam:
    @logged
    def instance_method(self):
        print("Instance method called")
        return "instance result"

    @classmethod
    @logged
    def class_method(cls):
        print("Class method called")
        return "class result"

    @staticmethod
    @logged
    def static_method():
        print("Static method called")
        return "static result"

    @property
    @logged
    def property_method(self):
        print("Property method called")
        return "property result"

В этой обновленной версии:

  • Для instance_method порядок не имеет значения. Методы экземпляра вызываются на экземпляре класса, и декоратор @logged может быть применен в любом порядке, не влияя на его базовую функциональность.
  • Для class_method мы применяем @classmethod после @logged. Декоратор @classmethod изменяет способ вызова метода, и применение его после @logged гарантирует, что логирование работает корректно.
  • Для static_method мы применяем @staticmethod после @logged. Подобно @classmethod, декоратор @staticmethod имеет свое собственное поведение, и порядок с декоратором @logged должен быть правильным.
  • Для property_method мы применяем @property после @logged. Это гарантирует, что поведение свойства сохраняется, а также обеспечивается функциональность логирования.
  1. Давайте протестируем обновленный код. Мы выполним ту же команду, что и раньше, чтобы проверить, были ли исправлены проблемы.
cd ~/project
python3 -c "from methods import Spam; s = Spam(); print(s.instance_method()); print(Spam.class_method()); print(Spam.static_method()); print(s.property_method)"

Теперь вы должны увидеть правильное логирование для всех типов методов:

Calling instance_method
Instance method called
instance result
Calling class_method
Class method called
class result
Calling static_method
Static method called
static result
Calling property_method
Property method called
property result

Лучшие практики при использовании декораторов методов

При работе с декораторами методов следуйте этим лучшим практикам:

  1. Применяйте декораторы, изменяющие метод (@classmethod, @staticmethod, @property) после своих пользовательских декораторов. Это гарантирует, что пользовательские декораторы могут выполнить свои операции по логированию или другим задачам сначала, а затем встроенные декораторы могут изменить метод как задумано.
  2. Будьте осведомлены о том, что выполнение декоратора происходит во время определения класса, а не во время вызова метода. Это означает, что любой код настройки или инициализации в декораторе будет выполнен при определении класса, а не при вызове метода.
  3. В более сложных случаях вам, возможно, придется создавать специализированные декораторы для разных типов методов. Разные типы методов имеют разное поведение, и универсальный декоратор может не работать во всех ситуациях.
✨ Проверить решение и практиковаться

Создание декоратора для принудительного применения типов с аргументами

В предыдущих шагах мы узнали о декораторе @validated. Этот декоратор используется для принудительного применения аннотаций типов в функциях Python. Аннотации типов - это способ указать ожидаемые типы аргументов функции и возвращаемого значения. Теперь мы пойдем дальше. Мы создадим более гибкий декоратор, который может принимать спецификации типов в качестве аргументов. Это означает, что мы можем определить типы, которые мы хотим для каждого аргумента и возвращаемого значения, более явно.

Понимание цели

Наша цель - создать декоратор @enforce(). Этот декоратор позволит нам задавать ограничения типов с использованием именованных аргументов. Вот пример того, как он будет работать:

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

В этом примере мы используем декоратор @enforce для указания того, что аргументы x и y функции add должны быть типа Integer, а возвращаемое значение также должно быть типа Integer. Этот декоратор будет вести себя аналогично нашему предыдущему декоратору @validated, но он дает нам больше контроля над спецификациями типов.

Создание декоратора enforce

  1. Сначала откройте файл validate.py в WebIDE. Мы добавим наш новый декоратор в этот файл. Вот код, который мы добавим:
from functools import wraps

class Integer:
    @classmethod
    def __instancecheck__(cls, x):
        return isinstance(x, int)

def validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        ## Get function annotations
        annotations = func.__annotations__
        ## Check arguments against annotations
        for arg_name, arg_value in zip(func.__code__.co_varnames, args):
            if arg_name in annotations and not isinstance(arg_value, annotations[arg_name]):
                raise TypeError(f'Expected {arg_name} to be {annotations[arg_name].__name__}')

        ## Run the function and get the result
        result = func(*args, **kwargs)

        ## Check the return value
        if 'return' in annotations and not isinstance(result, annotations['return']):
            raise TypeError(f'Expected return value to be {annotations["return"].__name__}')

        return result
    return wrapper

def enforce(**type_specs):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            ## Check argument types
            for arg_name, arg_value in zip(func.__code__.co_varnames, args):
                if arg_name in type_specs and not isinstance(arg_value, type_specs[arg_name]):
                    raise TypeError(f'Expected {arg_name} to be {type_specs[arg_name].__name__}')

            ## Run the function and get the result
            result = func(*args, **kwargs)

            ## Check the return value
            if 'return_' in type_specs and not isinstance(result, type_specs['return_']):
                raise TypeError(f'Expected return value to be {type_specs["return_"].__name__}')

            return result
        return wrapper
    return decorator

Давайте разберем, что делает этот код. Класс Integer используется для определения пользовательского типа. Декоратор validated проверяет типы аргументов функции и возвращаемого значения на основе аннотаций типов функции. Декоратор enforce - это новый, который мы создаем. Он принимает именованные аргументы, которые задают типы для каждого аргумента и возвращаемого значения. Внутри функции wrapper декоратора enforce мы проверяем, соответствуют ли типы аргументов и возвращаемого значения указанным типам. Если нет, мы вызываем исключение TypeError.

  1. Теперь давайте протестируем наш новый декоратор @enforce. Мы запустим несколько тестовых случаев, чтобы проверить, работает ли он как ожидается. Вот код для запуска тестов:
cd ~/project
python3 -c "from validate import enforce, Integer

@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y

## This should work
print(add(2, 3))

## This should raise a TypeError
try:
    print(add('2', 3))
except TypeError as e:
    print(f'Error: {e}')

## This should raise a TypeError
try:
    @enforce(x=Integer, y=Integer, return_=Integer)
    def bad_add(x, y):
        return str(x + y)
    print(bad_add(2, 3))
except TypeError as e:
    print(f'Error: {e}')"

В этом тестовом коде мы сначала определяем функцию add с декоратором @enforce. Затем мы вызываем функцию add с допустимыми аргументами, что должно работать без ошибок. Далее мы вызываем функцию add с недопустимым аргументом, что должно вызвать исключение TypeError. Наконец, мы определяем функцию bad_add, которая возвращает значение неправильного типа, что также должно вызвать исключение TypeError.

При запуске этого тестового кода вы должны увидеть вывод, похожий на следующий:

5
Error: Expected x to be Integer
Error: Expected return value to be Integer

Этот вывод показывает, что наш декоратор @enforce работает правильно. Он вызывает исключение TypeError, когда типы аргументов или возвращаемого значения не соответствуют указанным типам.

Сравнение двух подходов

Декораторы @validated и @enforce достигают одной и той же цели - принудительного применения ограничений типов, но они делают это разными способами.

  1. Декоратор @validated использует встроенные аннотации типов Python. Вот пример:

    @validated
    def add(x: Integer, y: Integer) -> Integer:
        return x + y

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

  2. С другой стороны, декоратор @enforce использует именованные аргументы для указания типов. Вот пример:

    @enforce(x=Integer, y=Integer, return_=Integer)
    def add(x, y):
        return x + y

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

Каждый подход имеет свои преимущества. Аннотации типов являются частью языка Python и обеспечивают лучшую поддержку в IDE, в то время как подход с @enforce дает нам больше гибкости и явности. Вы можете выбрать подход, который лучше всего подходит для ваших нужд, в зависимости от проекта, над которым вы работаете.

✨ Проверить решение и практиковаться

Резюме

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

Эти шаблоны декораторов широко используются в Python - фреймворках, таких как Flask, Django и pytest. Освоение декораторов позволит вам писать более поддерживаемый и переиспользуемый код. Чтобы продолжить свое обучение, вы можете изучить менеджеры контекста, декораторы на основе классов, использование декораторов для кэширования и расширенную проверку типов с помощью декораторов.