Определение простых декораторных функций

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

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

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

Введение

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

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python(("Python")) -.-> python/BasicConceptsGroup(["Basic Concepts"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python/BasicConceptsGroup -.-> python/type_conversion("Type Conversion") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/scope("Scope") python/AdvancedTopicsGroup -.-> python/decorators("Decorators") subgraph Lab Skills python/type_conversion -.-> lab-132514{{"Определение простых декораторных функций"}} python/function_definition -.-> lab-132514{{"Определение простых декораторных функций"}} python/scope -.-> lab-132514{{"Определение простых декораторных функций"}} python/decorators -.-> lab-132514{{"Определение простых декораторных функций"}} end

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

Что такое декораторы?

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

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

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

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

  1. Сначала откройте редактор VSCode. В директории /home/labex/project создайте новый файл с именем logcall.py. Этот файл будет содержать нашу функцию - декоратор.

  2. Добавьте следующий код в файл logcall.py:

## logcall.py

def logged(func):
    print('Adding logging to', func.__name__)
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Разберем, что делает этот код:

  • Функция logged является нашим декоратором. Она принимает другую функцию, которую мы называем func, в качестве аргумента. Эта функция func - это та функция, для которой мы хотим добавить логирование.
  • Когда декоратор применяется к функции, он выводит сообщение. Это сообщение сообщает, что логирование добавляется к функции с заданным именем.
  • Внутри функции logged мы определяем внутреннюю функцию с именем wrapper. Эта функция wrapper заменит исходную функцию.
    • Когда вызывается декорированная функция, функция wrapper выводит сообщение о том, что функция вызывается.
    • Затем она вызывает исходную функцию (func) со всеми аргументами, которые были переданы ей. *args и **kwargs используются для приема любого количества позиционных и ключевых аргументов.
    • Наконец, она возвращает результат выполнения исходной функции.
  • Функция logged возвращает функцию wrapper. Теперь эта функция wrapper будет использоваться вместо исходной функции, добавляя функциональность логирования.

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

  1. Теперь в той же директории (/home/labex/project) создайте еще один файл с именем sample.py со следующим кодом:
## sample.py

from logcall import logged

@logged
def add(x, y):
    return x + y

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

Синтаксис @logged здесь очень важен. Он сообщает Python, что необходимо применить декоратор logged к функциям add и sub. Таким образом, каждый раз, когда эти функции вызываются, будет выполняться функциональность логирования, добавленная декоратором.

Тестирование декоратора

  1. Чтобы протестировать ваш декоратор, откройте терминал в VSCode. Сначала измените директорию на директорию проекта с помощью следующей команды:
cd /home/labex/project

Затем запустите интерпретатор Python:

python3
  1. В интерпретаторе Python импортируйте модуль sample и протестируйте декорированные функции:
>>> import sample
Adding logging to add
Adding logging to sub
>>> sample.add(3, 4)
Calling add
7
>>> sample.sub(2, 3)
Calling sub
-1
>>> exit()

Обратите внимание, что при импорте модуля sample выводятся сообщения "Adding logging to...". Это происходит потому, что декоратор применяется при импорте модуля. Каждый раз, когда вы вызываете одну из декорированных функций, выводится сообщение "Calling...". Это показывает, что декоратор работает как ожидается.

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

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

Создание декоратора для валидации

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

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

Мы уже создали для вас файл с именем validate.py, который содержит некоторые классы валидации. Классы валидации используются для проверки, удовлетворяет ли значение определенным критериям. Чтобы посмотреть, что находится в этом файле, вам нужно открыть его в редакторе VSCode. Вы можете сделать это, выполнив следующие команды в терминале:

cd /home/labex/project
code validate.py

В файле есть три класса:

  1. Validator - Это базовый класс. Базовый класс предоставляет общую структуру или каркас, от которого могут наследоваться другие классы. В данном случае он предоставляет базовую структуру для валидации.
  2. Integer - Этот класс валидатора используется для проверки, является ли значение целым числом. Если вы передадите нецелое значение функции, которая использует этот валидатор, будет возбуждено исключение.
  3. PositiveInteger - Этот класс валидатора гарантирует, что значение является положительным целым числом. Поэтому, если вы передадите отрицательное целое число или ноль, также будет возбуждено исключение.

Добавление декоратора валидации

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

  1. Он будет анализировать аннотации типов функции. Аннотации типов - это как небольшие заметки, которые сообщают нам, какого рода данные ожидает функция.
  2. Он будет валидировать аргументы, переданные функции, на основе этих аннотаций типов. Это означает, что он проверит, являются ли значения, переданные функции, правильного типа.
  3. Он также будет валидировать возвращаемое значение функции на основе ее аннотации. Таким образом, он гарантирует, что функция возвращает тип данных, который должна возвращать.
  4. Если валидация не пройдена, он будет возбуждать информативные сообщения об ошибках. Эти сообщения будут точно сообщать вам, что пошло не так, например, какой аргумент имел неправильный тип.

Добавьте следующий код в конец файла validate.py:

## Add to validate.py

import inspect
import functools

def validated(func):
    sig = inspect.signature(func)

    print(f'Validating {func.__name__} {sig}')

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ## Bind arguments to the signature
        bound = sig.bind(*args, **kwargs)
        errors = []

        ## Validate each argument
        for name, value in bound.arguments.items():
            if name in sig.parameters:
                param = sig.parameters[name]
                if param.annotation != inspect.Parameter.empty:
                    try:
                        ## Create an instance of the validator and validate the value
                        if isinstance(param.annotation, type) and issubclass(param.annotation, Validator):
                            validator = param.annotation()
                            bound.arguments[name] = validator.validate(value)
                    except Exception as e:
                        errors.append(f'    {name}: {e}')

        ## If validation errors, raise an exception
        if errors:
            raise TypeError('Bad Arguments\n' + '\n'.join(errors))

        ## Call the function
        result = func(*bound.args, **bound.kwargs)

        ## Validate the return value
        if sig.return_annotation != inspect.Signature.empty:
            try:
                if isinstance(sig.return_annotation, type) and issubclass(sig.return_annotation, Validator):
                    validator = sig.return_annotation()
                    result = validator.validate(result)
            except Exception as e:
                raise TypeError(f'Bad return: {e}') from None

        return result

    return wrapper

Этот код использует модуль inspect Python. Модуль inspect позволяет получать информацию о живых объектах, таких как функции. Здесь мы используем его для анализа сигнатуры функции и валидации аргументов на основе аннотаций типов. Мы также используем functools.wraps. Это вспомогательная функция, которая сохраняет метаданные исходной функции, такие как ее имя и строка документации. Метаданные - это как дополнительная информация о функции, которая помогает нам понять, что она делает.

Тестирование декоратора валидации

Создадим файл для тестирования нашего декоратора валидации. Мы создадим новый файл с именем test_validate.py и добавим в него следующий код:

## test_validate.py

from validate import Integer, PositiveInteger, validated

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

@validated
def pow(x: Integer, y: Integer) -> Integer:
    return x ** y

## Test with a class
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def cost(self):
        return self.shares * self.price

    @validated
    def sell(self, nshares: PositiveInteger):
        self.shares -= nshares

Теперь мы протестируем наш декоратор в интерпретаторе Python. Сначала перейдите в директорию проекта и запустите интерпретатор Python, выполнив следующие команды в терминале:

cd /home/labex/project
python3

Затем в интерпретаторе Python мы можем выполнить следующий код для тестирования нашего декоратора:

>>> from test_validate import add, pow, Stock
Validating add (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating pow (x: validate.Integer, y: validate.Integer) -> validate.Integer
Validating sell (self, nshares: validate.PositiveInteger) -> <class 'inspect._empty'>
>>>
>>> ## Test with valid inputs
>>> add(2, 3)
5
>>>
>>> ## Test with invalid inputs
>>> add('2', '3')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    x: Expected <class 'int'>
    y: Expected <class 'int'>
>>>
>>> ## Test valid power
>>> pow(2, 3)
8
>>>
>>> ## Test with negative exponent (produces non - integer result)
>>> pow(2, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 83, in wrapper
    raise TypeError(f'Bad return: {e}') from None
TypeError: Bad return: Expected <class 'int'>
>>>
>>> ## Test with a class
>>> s = Stock("GOOG", 100, 490.1)
>>> s.sell(50)
>>> s.shares
50
>>>
>>> ## Test with invalid shares
>>> s.sell(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/labex/project/validate.py", line 75, in wrapper
    raise TypeError('Bad Arguments\n' + '\n'.join(errors))
TypeError: Bad Arguments
    nshares: Expected value > 0
>>> exit()

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

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

Резюме

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

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