Введение
В этом практическом занятии (лабораторной работе) вы узнаете о декораторах в Python, мощной функции, которая может изменять поведение функций и методов. Декораторы обычно используются для таких задач, как логирование, измерение производительности, контроль доступа и проверка типов.
Вы научитесь объединять несколько декораторов, создавать декораторы, принимающие параметры, сохранять метаданные функций при использовании декораторов и применять декораторы к различным типам методов класса. Файлы, с которыми вы будете работать, это logcall.py, validate.py и sample.py.
Сохранение метаданных функций в декораторах
В 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.
- Сначала откройте файл
logcall.pyв WebIDE. Вы можете перейти в директорию проекта, используя следующую команду в терминале:
cd ~/project
- Теперь обновите декоратор
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
Декоратор
@wraps(func)выполняет важную работу. Он берет все метаданные (например, имя, строку документации и аннотации) из исходной функцииfuncи присоединяет их к функции-оберткеwrapper. Таким образом, когда мы используем декорированную функцию, она будет иметь правильные метаданные.Давайте протестируем наш улучшенный декоратор. Выполните следующие команды в терминале:
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. Этот декоратор используется для проверки типов аргументов функции и возвращаемого значения на основе аннотаций функции.
Откройте файл
validate.pyв WebIDE.Обновите декоратор
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
- Давайте проверим, что наш декоратор
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), который принимает строку формата в качестве аргумента. Это позволит нам настроить сообщение логгирования.
- Откройте файл
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, которая является фактическим декоратором, модифицирующим целевую функцию.
- Теперь давайте протестируем наш новый декоратор, изменив файл
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 с пользовательской строкой формата.
- Запустите обновленный файл
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, используя его. Это поможет нам повторно использовать код и поддерживать единообразный формат логгирования.
- Обновите файл
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 с определенной строкой формата.
- Проверьте, что переопределенный декоратор
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, вероятно, используется для логирования информации о вызовах методов.
- Создайте новый файл
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.
- Давайте протестируем, как это работает. Мы выполним команду 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. Это гарантирует, что поведение свойства сохраняется, а также обеспечивается функциональность логирования.
- Давайте протестируем обновленный код. Мы выполним ту же команду, что и раньше, чтобы проверить, были ли исправлены проблемы.
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
Лучшие практики при использовании декораторов методов
При работе с декораторами методов следуйте этим лучшим практикам:
- Применяйте декораторы, изменяющие метод (
@classmethod,@staticmethod,@property) после своих пользовательских декораторов. Это гарантирует, что пользовательские декораторы могут выполнить свои операции по логированию или другим задачам сначала, а затем встроенные декораторы могут изменить метод как задумано. - Будьте осведомлены о том, что выполнение декоратора происходит во время определения класса, а не во время вызова метода. Это означает, что любой код настройки или инициализации в декораторе будет выполнен при определении класса, а не при вызове метода.
- В более сложных случаях вам, возможно, придется создавать специализированные декораторы для разных типов методов. Разные типы методов имеют разное поведение, и универсальный декоратор может не работать во всех ситуациях.
Создание декоратора для принудительного применения типов с аргументами
В предыдущих шагах мы узнали о декораторе @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
- Сначала откройте файл
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.
- Теперь давайте протестируем наш новый декоратор
@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 достигают одной и той же цели - принудительного применения ограничений типов, но они делают это разными способами.
Декоратор
@validatedиспользует встроенные аннотации типов Python. Вот пример:@validated def add(x: Integer, y: Integer) -> Integer: return x + yС помощью этого подхода мы задаем типы непосредственно в определении функции с использованием аннотаций типов. Это встроенная функция Python, и она обеспечивает лучшую поддержку в интегрированных средах разработки (IDE). IDE могут использовать эти аннотации типов для предоставления автодополнения кода, проверки типов и других полезных функций.
С другой стороны, декоратор
@enforceиспользует именованные аргументы для указания типов. Вот пример:@enforce(x=Integer, y=Integer, return_=Integer) def add(x, y): return x + yЭтот подход более явный, так как мы напрямую передаем спецификации типов в качестве аргументов декоратора. Он может быть полезен при работе с библиотеками, которые используют другие системы аннотаций.
Каждый подход имеет свои преимущества. Аннотации типов являются частью языка Python и обеспечивают лучшую поддержку в IDE, в то время как подход с @enforce дает нам больше гибкости и явности. Вы можете выбрать подход, который лучше всего подходит для ваших нужд, в зависимости от проекта, над которым вы работаете.
Резюме
В этом практическом занятии вы научились эффективно создавать и использовать декораторы. Вы узнали, как сохранять метаданные функций с помощью functools.wraps, создавать декораторы, принимающие параметры, работать с несколькими декораторами и понимать порядок их применения. Вы также научились применять декораторы к разным методам класса и создавать декоратор для принудительного применения типов, который принимает аргументы.
Эти шаблоны декораторов широко используются в Python - фреймворках, таких как Flask, Django и pytest. Освоение декораторов позволит вам писать более поддерживаемый и переиспользуемый код. Чтобы продолжить свое обучение, вы можете изучить менеджеры контекста, декораторы на основе классов, использование декораторов для кэширования и расширенную проверку типов с помощью декораторов.