Декораторы в Python позволяют изменять или расширять поведение функций или методов без изменения их фактического кода. Это хороший способ добавить поведение к многократно используемым функциям для синхронизации, кэширования, ведения журнала или аутентификации. В статье рассмотрим особенности создания декораторов, их расширенные возможности и применение в разных программах.
Введение: понимание концепции декораторов
Декораторы — это функции Python, которые позволяют оборачивать еще одну функцию в качестве входных данных и менять ее поведение. При этом код начальной функции остается прежним. Они используются для расширения поведения определенного объекта — класса, метода или функции. Этот подход способствует повторному использованию данных, модульности и разделению задач в программах.
Для понимания принципа работы decorators в Python, важно знать:
- зачем нужны функции;
- что такое первоклассные объекты;
- зачем нужны внутренние функции.
Функция возвращает значение на основе заданных аргументов. Например, здесь она возвращает двойное число:
def twice(number):
return number * 2
В Python функция — это объект первого класса. Это означает, что функция имеет все права, как и любая другая переменная в языке. Поэтому мы можем присвоить функцию переменной, передать ее функции или вернуть обратно.
Вы можете присвоить функцию переменной следующим образом:
def foo():
print("I am foo")
also_foo = foo
foo()
also_foo()
В Python все переменные — объекты. Определяемые нами имена — это просто идентификаторы, ссылающиеся на эти объекты. В примере и foo, и также _foo указывают на один и тот же объект.
Существует несколько вариантов использования функции как argument другой функции в Python. Например, использование ключевой функции для сортировки списков.
Function передают другой функции, как обычную переменную:
def do_twice(func):
func()
func()
def say_hello():
print("Hello!")
do_twice(say_hello)
Мы можем определять function внутри других функций. Их называют внутренними или вложенными. Декораторы в Python также используют их:
def parent():
print("I am the parent function")
def first_child():
print("I am the first child function")
def second_child():
print("I am the second child function")
first_child()
second_child()
Декораторы предоставляют гибкий и удобный способ добавить дополнительную функциональность к существующим функциям. Это делает их более универсальными и модульными.
Это полезно, если вы хотите добавить функцию к функции, которая используется в нескольких местах кода, но не хотите менять исходный вариант.
Создание и использование простых декораторов
Синтаксис:
func = decorator(func),
где func — это декорируемая function, а decorator — это функция, используемая для ее обертки.
Команда начинается с ключевого слова «def», обозначающего функцию. За ним следует (@) и имя декоратора. После этого можно добавить любые необходимые аргументы, а затем передать целевую function в виде аргумента. Пример:
#Step 1: Define the decorator function def decorator_name(target_function):
#Step 2: Define the wrapper function def wrapper( * args, ** kwargs):
#Step 3: Do something before target_function is called results = target_function( * args, ** kwargs)
# Step 4: Do something after the call to target_function
return results
return wrapper
Декораторы позволяют расширить функциональность без внесений изменений в код. Для этого используются функции высшего порядка. Они принимают function в виде входных данных. Затем возвращают новую функцию с обновленным поведением.
def simple_decorator(func):
def wrapper():
print("Before function execution")
func()
print("After function execution")
return wrapper
@simple_decorator
def greet():
print("Hello, world!")
greet()
Передача аргументов в декораторы
Декораторы с аргументами обеспечивают дополнительную настройку и гибкость при изменении или расширении поведения функций или классов. Эти декораторы принимают аргументы и возвращают другую функцию-декоратор. Она принимает function или класс в качестве входных данных.
def decorator_func(initial_func):
def wrapper_func(msg):
print("wrapper function picked some...")
return initial_func()
return wrapper_func
@decorator_func
def prettify(msg):
print(msg)
prettify("flowers for you")
Вложенные декораторы
Вложенные декораторы также часто используются для выполнения классических задач в проектах. Вложенность означает размещение или хранение внутри другого. Следовательно, вложенные декораторы означают применение более одного декоратора внутри функции. Python предоставляет возможность реализовать более одного декоратора для функции. Это делает декораторы полезными для многократного использования строительных блоков. Так они состоят из нескольких функций вместе.
Вложенные декораторы еще называют цепочками. Чтобы создать такой вид, сначала нам нужно определить декоратор, которым мы хотим обернуть выходную строку. Затем применить его к функции с использованием pie syntax(@ знак).
@function1
@function2
def function(name):
print(f"{name}")
Для одного подхода есть два декоратора, которые будут выполняться снизу вверх, то есть в обратном порядке. Это похоже на строительство дома: сначала закладывают фундамент, потом кладут пол и возводят стену.
Рассмотрим пример:
def lower(func):
def wrapper():
return func().lower()
return wrapper
def upper(func):
def wrapper():
return func().upper() + ' OF NESTED DECORATORS.'
return wrapper
@lower
@upper
def message():
return 'This is a Basic program'
print(introduction())
Сначала мы определили две функции-декоратора, которые используются для переноса выходной строки в function строки «lower()» и «upper()».
Мы применили к message() два декоратора, используя '@' и имя функции. В этой программе мы используем @upper и @lower.
Они выполняются снизу вверх. Поэтому строка сначала меняется на «FOR DECORATOR», а затем все строки преобразуются в нижний регистр.
Расширенные возможности декораторов
@classmethod — это декоратор, используемый для определения методов класса внутри класса. Методы класса привязаны к классу, а не к объекту. Основное различие между статическими методами и методами класса — в их взаимодействии с состоянием. Методы класса имеют доступ к состоянию класса и могут изменять его. Статические же методы не могут получить доступ к состоянию класса и работать независимо.
Пример:
class Student:
total_students = 0
def __init__(self, name, age):
self.name = name
self.age = age
Student.total_students += 1
@classmethod
def increment_total_students(cls):
cls.total_students += 1
print(f"Class method called. Total students now: {cls.total_students}")
# Creating instances of the class
student1 = Student(name="Tom", age=20)
student2 = Student(name="Cruise", age=22)
# Calling the class method
Student.increment_total_students() #Total students now: 3
# Accessing the class variable
print(f"Total students from student 1: {student1.total_students}")
print(f"Total students from student 2: {student2.total_students}")
Выход:
Class method called. Total students now: 3
Total students from student 1: 3
Total students from student 2: 3
Здесь класс Student имеет переменную класса total_students. Декоратор @classmethod используется для определения метода класса ignore_total_students() для увеличения переменной total_students. Когда создается экземпляр класса Student, общее количество студентов увеличивается на единицу. Мы создали два экземпляра класса, а затем использовали метод, чтобы изменить переменную total_students на 3.
Есть decorators с возвращаемым значением. Рассмотрим следующую функцию добавления: она вводит оператор, а затем возвращает сумму двух чисел. Мы используем do_twice:
import functools
def do_twice(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
@do_twice
def add(num1, num2):
print(f"Adding {num1} and {num2}")
return num1 + num2
print("The sum is:", add(1, 2))
Выход:
Adding 1 and 2
Adding 1 and 2
The sum is: None
Функция add была вызвана дважды, как ожидалось, но в возвращаемом значении мы получили None. Это связано с тем, что wrapper function не возвращает никакого значения.
Можно создать вариант для измерения времени выполнения функции и метода экземпляра в классе:
from functools import wraps
import time
def timeit(func):
@wraps(func)
def timeit_wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
total_time = end_time - start_time
print(f'Function {func.__name__}{args} {kwargs} Took {total_time:.4f} seconds')
return result
return timeit_wrapper
@timeit
def calculate_something(num):
"""
Simple function that returns sum of all numbers up to the square of num.
"""
total = sum((x for x in range(0, num**2)))
return total
if __name__ == '__main__':
calculate_something(10)
calculate_something(100)
calculate_something(1000)
calculate_something(5000)
calculate_something(10000)
Сначала мы оформляем функцию декоратором timeit. Он отмечает время начала,
затем выполняет function. Decorator также отмечает время окончания. Затем он вычисляет разницу во времени. На выходе мы увидим время, затраченное на выполнение функции.
Применения декораторов в реальных сценариях
Авторизация и аутентификация. Decorators можно использовать для реализации контроля доступа к функциям или методам. Это помогает понять, аутентифицирован ли пользователь или авторизован ли он. Пример:
from enum import Enum, auto
class Permissions(Enum):
"""Permissions constants"""
CREATE = auto()
READ = auto()
UPDATE = auto()
DELETE = auto()
def permission_required(permission):
"""Decorator for checking permissions"""
def outer_wrapper(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
is_authorized = False
keys = kwargs.keys()
if "items_list" in keys:
target = kwargs["items_list"]
elif "list_item" in keys:
target = kwargs["list_item"]
is_authorized = target.check_user_permission(
kwargs["user"], permission
)
if not is_authorized:
return
return fn(*args, **kwargs)
return wrapper
return outer_wrapper
@load_user_or_fail
@load_list_or_fail(source="user_data")
@permission_required(Permissions.UPDATE)
def message_rename_list(
update: Update,
context: CallbackContext,
user: User,
items_list: ItemsList
) -> int:
# Update list name
Ведение журнала и обработка ошибок. Decorators подходят для регистрации информации о вызовах функций или для согласованной обработки ошибок:
import logging
def logging_decorator(func):
def wrapper( * args, ** kwargs):
try:
logging.info(f "Calling {func.__name__} with arguments {args} and keyword arguments {kwargs}")
result = func( * args, ** kwargs)
logging.info(f "Function {func.__name__} executed successfully")
return result
except Exception as e:
logging.error(f "An error occurred while executing {func.__name__}: {e}")
raise
return wrapper
@logging_decorator
def example_function(a, b):
return a / b
example_function(10, 5)
example_function(10, 0)
Кэширование результатов функций. Можно написать функцию, которая вычисляет n-е число Фибоначчи. Рекурсивная реализация последовательности Фибоначчи:
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
Без кэширования рекурсивные вызовы приводят к избыточным вычислениям. Было бы эффективнее искать кэшированные значения. Для этого можно использовать декоратор @cache.
Декоратор @cache из модуля functools в Python 3.9+ используется для кэширования результатов функции. Он сохраняет результаты вызовов функций и повторно использует их, когда функция вызывается с теми же аргументами. Пример:
from functools import cache
@cache
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
Заключение
Декораторы динамически изменяют состояние функции, метода или класса без необходимости напрямую использовать подклассы или изменять исходный код . Использование декораторов в Python также гарантирует, что код будет DRY(Don't Repeat Yourself). У декораторов есть несколько вариантов использования:
- авторизация в средах Python, таких как Flask и Django;
- ведение журнала;
- измерение времени выполнения;
- синхронизация.