Использование паттерна декоратор позволяет изменить поведение и расширить функциональность уже рабочего кода не переписывая его заново. В сети можно найти достаточно много материалов описывающих синтаксис декораторов в Python, однако статей поясняющих, что же находится “под капотом”, где рассматриваются интересные примеры их применения на практике, не так уж и много. Перевод статьи Untangling Python Decorators.
В этой статье, как вы уже наверное поняли, мы займемся исследованием анатомии декораторов, использующихся при программировании на языке Python.
Когда я впервые узнал о декораторах в Python, то мне показалось, что их использование более похоже на магию вуду. Как известно, декораторы могут дать вам возможность добавлять новый функционал к вызову любой функции, не изменяя код внутри ее. Как правило, этот прием позволяет рациональнее реализовать инкапсуляцию вашего кода, то есть помогает писать его более чистым и понятным. Тем не менее, использование декораторов считается довольно продвинутой темой в Python, поскольку их понимание и написание требует от вас использования других дополнительных понятий, таких как объекты первого класса и функции высокого порядка. И так я попытаюсь представить вашему вниманию эти ключевые понятия по мере необходимости, а затем шаг за шагом приступим к рассмотрению основ концепции декораторов в Python. И так начнем.
Содержание
- Объекты первого класса
- Функции высшего порядка
- Замыкания
- Пишем декоратор сами
- Используем символ @ (синтаксический сахар )
- Используем аргументы при декорировании функций
- Решение проблемы идентификации функций
- Декораторы в дикой природе (практика)
- Применяем сразу несколько декораторов
- Используем декораторы с аргументами
- Декораторы с аргументами и без
- Пишем универсальный шаблон для декоратора
- Определяем декораторы с помощью классов
- Еще несколько примеров
- Регистрация функции для логирования выполнения кода
- Небольшое дополнение
Объекты первого класса
Как вам, наверное, известно в Python практически всё является объектом, а функции рассматриваются как объекты первого класса. Это означает, что функции могут передаваться и использоваться в качестве аргументов, как и любой другой объект (например, строка, int, float, список и т.д.). Также функции можно присваивать переменным, то есть рассматривать их как любые другие объекты. Рассмотрим следующий пример:
def func_a(): return "I was angry with my friend." def func_b(): return "I told my wrath, my wrath did end" def func_c(*funcs): for func in funcs: print(func()) main_func = func_c main_func(func_a, func_b)
>>> I was angry with my friend. >>> I told my wrath, my wrath did end
Этот пример кода иллюстрирует тот факт, что функции в Python рассматриваются как объекты первого класса. В начале я определяю две функции, func_a
и func_b
, а затем функцию func_c
, которая принимает их в качестве своих параметров. func_c
запускает, принятые в качестве параметров функции на выполнение, и выводит в консоли результаты их работы. Затем мы присваиваем функцию func_c
переменной main_func
. В итоге мы запускаем функцию main_func()
и она ведет себя точно так же, как и func_c
.
Функции высшего порядка
Python позволяет использовать функции в качестве возвращаемых значений. Вы можете взять некоторую функцию и вернуть ее из другой функции или определить функцию в другой функции, а затем вернуть эту внутреннюю функцию.
def higher(func): """Это функция высшего порядка. Она возвращает другую функцию. """ return func def lower(): return "I'm hunting high and low" higher(lower)
>>>
Теперь вы можете присвоить результат выполнения функции higher
другой переменной и выполнить возвращаемую ею внутреннюю функцию.
h = higher(lower) h()
>>> "I'm hunting high and low"
Давайте рассмотрим другой пример, в своем коде вы можете определить некоторую вложенную функцию внутри другой, а затем вернуть вложенную функцию в качестве результата выполнения первой.
def outer(): """Определим и вернем вложенную функцию из другой функции.""" def inner(): return "Привет из внутренней func" return inner inn = outer() inn()
>>> 'Hello from the inner func'
Обратите внимание, что внутренняя функция inner
была определена внутри внешней функции outer
, а затем оператор return
функции outer
возвращает вложенную функцию. После определения основной функции outer
для того, чтобы получить вложенную функцию, необходимо сначала вызвать функцию outer
и тогда, в качестве возвращаемого ей результата, получим внутреннюю функциюinner
. И только последующее выполнение результата, возвращаемого функцией outer
, выведет сообщение из функции inner
.
Замыкания
Примеры внутренних функций, определяемых в других функциях, мы разобрали в предыдущем разделе. Такие вложенные функции могут получить доступ к переменным из области видимости внешней (оборачивающей ее) функции. В Python по умолчанию предусмотрена возможность использования в функциях таких нелокальных переменных, и если мы хотим изменять их значения в коде внутренней, то должны объявить их нелокальными non-local явно (с ключевым словом nonlocal
). Ниже приведён пример вложенной функции, получающей нелокальную переменную из внешней (по умолчанию без использования ключевого слова nonlocal
).
def burger(name): def ingredients(): if name == "deli": return ("steak", "pastrami", "emmental") elif name == "smashed": return ("chicken", "nacho cheese", "jalapeno") else: return None return ingredients
Теперь запустим следующий код на выполнение:
ingr = burger("deli") dish = ingr() print(dish)
>>> ('steak', 'pastrami', 'emmental')
Все работает так, как и было задумано. Значение параметра внешней функции name
передается во внутреннюю.
Вначале функция burger
была вызвана со строковым параметром deli
, а затем возвращённая ею функция была присвоена переменной ingr
. При вызове инструкции ingr()
, переданное ранее значение name
сохранилось и в последствии использовалось для получения результата, хотя внешняя функция burger
уже закончила свое выполнение.
Этот способ, с помощью которого,как в примере выше, строковые данные deli
присоединяются к исполняемому коду, называется замыканием.
Замыкание – это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. Синтаксически это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.
Значение переменной из области видимости внешней функции запоминается даже в том случае, если сама переменная выходит из текущей области видимости, а также если сама (внешняя) функция удаляется из текущего контекста исполнения (пространства имен). Декораторы в своей работе используют тот же принцип применения нелокальных non-local переменных и скоро вы убедитесь в этом сами.
Пишем декоратор сами
И так вооруживший, полученными выше, знаниями о принципе действия нелокальных non-local переменных, создадим наш первый самый простейший декоратор.
def deco(func): def wrapper(): print("Это сообщение будет напечатано до вызова функции.") func() print("Это сообщение будет напечатано после вызова функции.") return wrapper
Прежде чем использовать наш декоратор, давайте определим обыкновенную функцию без каких-либо параметров.
def ans(): print(42)
Рассматривая функции как объекты первого класса, вы можете использовать свой декоратор следующим образом:
ans = deco(ans) ans()
Ниже приведен результат выполнения этого кода.
>>> Это сообщение будет напечатано до вызова функции. 42 Это сообщение будет напечатано после вызова функции.
В двух строках кода выше мы можем наблюдать наш простейший декоратор в действии. Функция deco
принимает другую функцию в качестве своего параметра, манипулирует этой функцией внутри функции-обёртки и затем возвращает функцию-обёртку. При выполнении функции, возвращаемой нашим декоратором, вы получите модифицированный результат ее выполнения. Проще говоря, декораторы оборачивают декорируемую функцию и изменяют ее поведение, не изменяя ее кода.
Функция декоратора выполняется во время импорта/определения декорированной функции, а не при ее вызове.
Прежде чем перейти к следующему разделу, давайте посмотрим, как мы ещё можем получать возвращаемое целевой функцией значение, а не просто распечатывать результат.
def deco(func): """Этот модифицированный декоратор также возвращает результат функции func.""" def wrapper(): print("Это сообщение будет напечатано до вызова функции.") val = func() print("Это сообщение будет напечатано после вызова функции.") return val return wrapper def ans(): return 42
В примере выше функция-обертка возвращает результат целевой функции и выполнения кода обертки. Этот прием позволяет получить модифицированной результат выполнения целевой функции.
ans = deco(ans) print(ans())
>>> Это сообщение будет напечатано до вызова функции. Это сообщение будет напечатано после вызова функции. 42
Посмотрите, теперь возвращаемое значение декорируемой функции появилось в последней строке, а не в середине сообщения, как было ранее.
Используем символ @ (синтаксический сахар )
То, как вы использовали декоратор в последнем разделе, может показаться немного неуклюжим. Во-первых, нам необходимо использовать имя декорируемой (целевой) функции ans
три раза для того, чтобы вызвать и использовать наш декоратор. Кроме того, становится труднее понять, где в коде наш декоратор на самом деле вызывается, и что его функциональное назначение декорирование целевых функций – код становится трудно читаемым и непонятным. Поэтому в Python предусмотрена возможность использования декораторов в вашем коде с применением специального синтаксиса с символом @
. И вы можете использовать декораторы при определении своих функций, как это показано в примере ниже:
@deco def func(): # ... # код нашей декорируемой функции # теперь можно вызвать нашу декорируемую функцию как обычную func()
Часто этот синтаксис называют pie syntax (по аналогии со слоями пирога). Его использование повышает читаемость кода, однако его использование по сути является ни чем иным, как синтаксическим сахаром, использующимся вместо, рассмотренной нами выше, инструкции func = deco (func)
.
Используем аргументы при декорировании функций
Наш простейший декоратор, который мы реализовали выше, будет работать только для функций, которые не требуют передачи в них параметров. И вызов нашей функции с параметрами потерпит неудачу и приведет к возбуждению исключения типа TypeError, если мы попытаемся декорировать функцию, принимающую аргументы из функции deco
(декоратора). Давайте создадим другой декоратор и назовем его yell
, он в качестве параметра принимает функцию, которая, в свою очередь, возвращает строку, преобразованную ее в верхний регистр.
def yell(func): def wrapper(*args, **kwargs): val = func(*args, **kwargs) val = val.upper() + "!" return val return wrapper
Определим целевую функцию, которая будет возвращать строковое значение, и которую мы далее будем декорировать.
@yell def hello(name): return f"Hello {name}"
hello("redowan")
>>> 'HELLO REDOWAN!'
Функция hello
принимает строку в качестве параметра name
и возвращает сообщение в виде трансформированной строки. И так наш декоратор yell
изменяет возвращаемую целевой функций строку, преобразует ее в верхний регистр и добавляет символ !
без непосредственного изменения кода в функции hello
.
Решение проблемы идентификации функций
В Python вы можете исследовать любой объект и его свойства с помощью интерактивной оболочки. Функция предоставляет информацию о себе, используя, например, такой способ самодокументирования кода как docstring и т. д. Исследуем информацию о встроенной функции print
следующими способами:
>>>
print.__name__
>>> 'print'
print.__doc__
>>> "print(value, ..., sep=' ', end='\\n', file=sys.stdout, flush=False)\n\nPrints the values to a stream, or to sys.stdout by default.\nOptional keyword arguments:\nfile: a file-like object (stream); defaults to the current sys.stdout.\nsep: string inserted between values, default a space.\nend: string appended after the last value, default a newline.\nflush: whether to forcibly flush the stream."
help(print)
>>> Help on built-in function print in module builtins: print(...) print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False) Prints the values to a stream, or to sys.stdout by default. Optional keyword arguments: file: a file-like object (stream); defaults to the current sys.stdout. sep: string inserted between values, default a space. end: string appended after the last value, default a newline. flush: whether to forcibly flush the stream.
Эти способы получения информации работают аналогично для пользовательских функций, которые вы определили бы сами. Давайте применим их для получения информации о ранее определенной нами функции hello()
.
hello.__name__
>>> 'wrapper'
help(hello)
>>> Help on function wrapper in module __main__: wrapper(*args, **kwargs)
Теперь проанализируем то, что же все таки произошло. Декоратор yell
, который мы написали для нашей функции hello
, приводит нас в заблуждение в отношении ее идентификации. Вместо того, чтобы сообщать нам свое имя, она выдает в консоли идентификатор функции wrapper
. Этот факт может сбивать с толку при выполнении отладки вашего кода. Это можно исправить с помощью декоратора импортируемого из модуля functools.wraps
. Следующий прием позволит сохранить первоначальную идентичность декорируемой функции.
import functools def yell(func): @functools.wraps(func) def wrapper(*args, **kwargs): val = func(*args, **kwargs) val = val.upper() + "!" return val return wrapper @yell def hello(name): "Hello from the other side." return f"Hello {name}"
hello("Galaxy")
>>> 'HELLO GALAXY!'
Анализ кода функции hello
, к которой был применен этот декоратор, дает нам желаемый результат.
hello.__name__
>>> 'hello'
help(hello)
>>> Help on function hello in module __main__: hello(name) Hello from the other side.
Декораторы в дикой природе (практика)
Перед тем как перейти к следующему разделу давайте рассмотрим несколько реальных примеров декораторов. Для того, чтобы определять свои декораторы далее мы будем использовать шаблон кода, который ранее уже последовательно усовершенствовали.
import functools def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): # Делаем что-то до val = func(*args, **kwargs) # Делаем что-то после return val return wrapper
Таймер
Декоратор таймер поможет измерить продолжительность во времени вызовов ваших функций простым и явным способом. Следующий пример кода применим для отладке и профайлинге Profiling пользовательских функций.
import time import functools def timer(func): """Этот декоратор выведет в консоли время выполнения вызываемого кода.""" @functools.wraps(func) def wrapper(*args, **kwargs): start_time = time.time() val = func(*args, **kwargs) end_time = time.time() run_time = end_time - start_time print(f"Выполнение функции {func.__name__} завершено через {run_time:.4f} секунд.") return val return wrapper @timer def dothings(n_times): for _ in range(n_times): return sum((i ** 3 for i in range(100_000)))
Представленным выше способом мы сможем анализировать время, необходимое для выполнения функции.
dothings(100_000)
>>> Выполнение функции dothings завершено через 0.0231 секунд. 24999500002500000000
Логирование исключений
Точно так же, как в примере с декоратором таймера, мы можем определить декоратор для логирования ошибок выполнения кода, который будет регистрировать состояние его выполнения. Для демонстрации его работы я написал логгер, вызываемых в ходе выполнения кода исключений, который будет отображать дополнительную информацию: метку текущего времени, наименования и значения передаваемых в функцию аргументов.
import functools from datetime import datetime def logexc(func): @functools.wraps(func) def wrapper(*args, **kwargs): # Преобразуем в строку имена аргументов и их значения args_rep = [repr(arg) for arg in args] kwargs_rep = [f"{k}={v!r}" for k, v in kwargs.items()] sig = ", ".join(args_rep + kwargs_rep) # Определяем блок Try для кода, который будем логировать try: return func(*args, **kwargs) except Exception as e: print("Time: ", datetime.now().strftime("%Y-%m-%d [%H:%M:%S]")) print("Arguments: ", sig) print("Error:\n") raise return wrapper @logexc def divint(a, b): return a / b
Давайте зададим значения для параметров функции, которое вызовет генерацию исключения ZeroDivisionError
, чтобы проверить наш логгер в работе.
divint(1, 0)
>>> Time: 2020-05-12 [12:03:31] Arguments: 1, 0 Error: ------------------------------------------------------------ ZeroDivisionError Traceback (most recent call last) ....
Как видим, декоратор сначала выводит в консоль сведения о вызываемой функции, а затем выдает информацию об исходной ошибке, вызвавшей генерацию исключения.
Валидация и проверки во время выполнения кода
Система типов в Python достаточно строга, но при этом весьма гибка. Несмотря на все свои преимущества, это означает, что некоторые ошибки могут все же закрасться в пользовательский код, и обычно более строго типизированные языки (такие как Java) будут отлавливать эти ошибки во время компиляции. И поэтому при написании своего кода вы захотите применять более сложные, настраиваемые проверки корректности входящих и исходящих данных. Использование декораторов поможет вам более легко справиться с этой задачей, а также применять ваши проверки к нескольким функциям одновременно.
Представьте себе, что у вас есть набор функций, каждая из которых возвращает словарь с данными, который (среди прочих полей) включает в себя поле summary
(резюме, сводка). Значение этого поля не должно иметь длину свыше 30 символов, и если это не выполняется то, это будем считать ошибкой. Следующий пример кода реализует декоратор, который вызывает исключение типа ValueError, если проверка длинны поле summary
не проходит:
import functools def validate_summary(func): @functools.wraps(func) def wrapper(*args, **kwargs): data = func(*args, **kwargs) if len(data["summary"]) > 30: raise ValueError("Ваше резюме превышает 30 символов.") return data return wrapper @validate_summary def short_summary(): return {"summary": "Этот резюме короткое"} @validate_summary def long_summary(): return {"summary": "Это резюме слишком длинное оно превышает лимит символов."} print(short_summary()) print(long_summary())
>>> {'summary': 'Этот резюме короткое'} ------------------------------------------------------------------- ValueError Traceback (most recent call last)in 19 20 print(short_summary()) ---> 21 print(long_summary()) ...
Повторитель выполнения функций
Теперь представим ситуацию, когда вызов пользовательской функции, терпит неудачу из-за проблем, связанных, например, с процессом ввода/вывода данных, и вы хотели бы повторить ее выполнение несколько раз. Представляемый вашему вниманию декоратор повторитель позволит многократно выполнять ваш код заданное число раз или до получения успешного результата. И так давайте определим декоратор retry
, который будет повторно запускать нашу декорируемую функцию 3 раза, пока будет возникать ошибка соединения с удаленным сервисом по протоколу http.
import functools import requests def retry(func): """Этот код перезапустит декорируемую функцию 3 раза пока будет получаться ошибка http 500/404.""" @functools.wraps(func) def wrapper(*args, **kwargs): n_tries = 3 tries = 0 while True: resp = func(*args, **kwargs) if resp.status_code == 500 or resp.status_code == 404 and tries>>> retrying... (0) retrying... (1) retrying... (2) '\n404 Not Found \nNot Found
\nThe requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
\n'Применяем сразу несколько декораторов
Вы можете применить несколько декораторов к функции, “накладывая” их друг на друга. Давайте определим два простых декоратора и используем их оба для одной целевой функции.
import functools def greet(func): """Приветствуем на английском.""" @functools.wraps(func) def wrapper(*args, **kwargs): val = func(*args, **kwargs) return "Hello " + val + "!" return wrapper def flare(func): """Кое-что добавим в нашу строку.""" @functools.wraps(func) def wrapper(*args, **kwargs): val = func(*args, **kwargs) return "🎉 " + val + " 🎉" return wrapper @flare @greet def getname(name): return name getname("Nafi")>>> '🎉 Hello Nafi! 🎉'Декораторы выполняются в порядке снизу вверх. Сначала будет выполнен декоратор
greet
, который применяется к результату, возвращенному функциейgetname
, а затем результат выполнения кода декоратораgreet
передается кодуflare
. Стек применения декораторов из примера выше может быть в функциональном стиле переписан в следующем виде:flare(greet (getname (name)))
. Самостоятельно измените порядок декораторов и посмотрите, что получится!Используем декораторы с аргументами
При определении декоратора повторителя
retry
в предыдущем разделе вы, возможно, заметили, что я жестко задал количество попыток повторного запуска функции в случае получения ошибок. Было бы удобно, если бы вы смогли передать число попыток как параметр в наш декоратор и заставить его работать соответствующим образом. Это, действительно, не тривиальная задача и для этого нам понадобятся целых три уровня вложенных функций.Прежде мы сделаем это рассмотрим простой пример того, как можно определять декораторы с параметрами.
import functools def joinby(delimiter=" "): """Этот декоратор разбивает строку, возвращаемую декорируемой функцией, по пробельному символу, а затем соединяет ее части с разделителем, переданным пользователем в качестве параметра.""" def outer_wrapper(func): @functools.wraps(func) def inner_wrapper(*args, **kwargs): val = func(*args, **kwargs) val = val.split(" ") val = delimiter.join(val) return val return inner_wrapper return outer_wrapper @joinby(delimiter=",") def hello(name): return f"Hello {name}!" @joinby(delimiter=">") def greet(name): return f"Greetings {name}!" @joinby() def goodbye(name): return f"Goodbye {name}!" print(hello("Nafi")) print(greet("Redowan")) print(goodbye("Delowar"))>>> Hello,Nafi! Greetings>Redowan! Goodbye Delowar!Декоратор
joinby
принимает один параметрdelimiter
. Код декоратора разбивает на части строку, возвращаемую декорируемой функцией по символу одинарный пробел, а затем соединяет их, используя определенный пользователем разделитель, передаваемый в аргументdelimiter
. Трехуровневое вложенное определение выглядит необычно и мы скоро к этому вернемся. Подумайте, как вы могли бы использовать декоратор с параметрами.В приведенном выше примере я определил три функции, чтобы продемонстрировать работу декоратора
joinby
. Важно помнить, что если декоратор принимает параметры, то вам нужно всегда передавать им значения, а если вы не хотите это делать, то определите для них значения по умолчанию.Как мы уже знаем, декоратор создает и возвращает свою внутреннюю функцию-обертку, по аналогии в декораторе
repeat
внутренняя функция помещается внутри другой внутренней функции. И это чем-то напоминает сон во сне из фильма «Начало».В коде реализации функции
joinby
присутствует несколько важных, но неочевидных деталей:
- определение
external_wrapper
в качестве внутренней функции означает, чтоrepeat
будет ссылаться на объект функцииexternal_wrapper
. - аргумент
delimiter
, как очевидно, не используется вjoinby
. Но, при передаче разделителяdelimiter
, создается замыкание, в котором его значение сохраняется до тех пор, пока оно не будет использовано в функцииinner_wrapper
.
Декораторы с аргументами и без
Ранее мы говорили, что наш декоратор, получающий параметры не может без них использоваться. Но что делать, если вы хотите создать такой декоратор, который можно использовать как с аргументами, так и без них. Для решения этой задачи перепишем декоратор joinby
, чтобы при необходимости мы могли передавать в него параметры или использовать без них как обычный декоратор.
import functools def joinby(_func=None, *, delimiter=" "): """Этот декоратор разбивает строку, возвращаемую декорируемой функцией, по пробельному символу, а затем соединяет ее части с разделителем, переданным пользователем в качестве параметра.""" def outer_wrapper(func): @functools.wraps(func) def inner_wrapper(*args, **kwargs): val = func(*args, **kwargs) val = val.split(" ") val = delimiter.join(val) return val return inner_wrapper # Эта часть позволяет использовать декоратор с параметрами или без if _func is None: return outer_wrapper else: return outer_wrapper(_func) @joinby(delimiter=",") def hello(name): return f"Hello {name}!" @joinby def greet(name): return f"Greetings {name}!" print(hello("Nafi")) print(greet("Redowan"))
>>> Hello,Nafi! Greetings Redowan!
В этом примере параметр _func
играет роль маркера, определяя, был ли декоратор вызван с параметрами или без.
Если joinby
вызывается без аргументов, то функция, к которой мы применяем декоратор, будет передаваться как _func
. Если же он был вызван с аргументами, то параметр _func
принимает значение None
. Символ *
в списке аргументов означает, что остальные аргументы могут быть вызваны как не позиционные. На этот раз вы можете использовать joinby
с аргументами или без них, и вывод результатов вызовов функций hello
и greet
проиллюстрирует это.
Пишем универсальный шаблон для декоратора
Лично я нахожу излишним и обременительным то, что нам нужно использовать три слоя вложенных функций для определения более совершенного декоратора, который мы можем использовать с аргументами или без. Дэвид Бизли в своей книге Python Cookbook предлагает отличный способ для определения универсальных декораторов без использования трех уровней вложенных функций. Для этого используется функционал модуля functools.partial
. Ниже приведен пример кода, который можно использовать для определения универсальных декораторов более элегантным способом:
import functools def decorator(func=None, foo="spam"): if func is None: return functools.partial(decorator, foo=foo) @functools.wraps(func) def wrapper(*args, **kwargs): # Тут вы можете что-то сделать с `func` или `foo` pass return wrapper # Применяем декоратор без параметров @decorator def f(*args, **kwargs): pass # Применяем декоратор с параметрами @decorator(foo="buzz") def f(*args, **kwargs): pass
Давайте перепишем наш декоратор retry
, используя этот шаблон кода.
import functools def retry(func=None, n_tries=4): if func is None: return functools.partial(retry, n_tries=n_tries) @functools.wraps(func) def wrapper(*args, **kwargs): tries = 0 while True: resp = func(*args, **kwargs) if resp.status_code == 500 or resp.status_code == 404 and tries>>> retrying... (0) retrying... (1) retrying... (2) retrying... (3) ----------------------- retrying... (0) retrying... (1)И так, теперь нам не нужно писать три уровня вложенных функций, функция
functools.partial
позаботится об этом. Методpartial
может быть использован для создания новых производящих функций, которым передаются некоторые входные параметры для инициализации. При этомpartial
будет выполнять следующий код:def partial(func, *part_args): def wrapper(*extra_args): args = list(part_args) args.extend(extra_args) return func(*args) return wrapperОпределяем декораторы с помощью классов
В этом разделе мы рассмотрим как использовать классы для создания декораторов. И так, классы могут быть очень полезны, так как позволяют избежать вложенной архитектуры при определении декораторов. Кроме того полезно использовать классы для написания декораторов с сохранением некоторого внутреннего состояния от вызова к вызову. Ниже приведен пример кода, который поясняет как можно реализовать декоратор с использованием классов.
class ClassDeco: def __init__(self, func): functools.update_wrapper(self, func) self.func = func def __call__(self, *args, **kwargs): # Здесь вы можете добавить свой код перед вызовом функции val = self.func(*args, **kwargs) # Здесь вы можете добавить свой код после вызова функции return valДавайте используем этот шаблон кода, чтобы написать декоратор
Emphasis
, который будет добавлять тегик строке, возвращаемой функцией.
import functools class Emphasis: def __init__(self, func): functools.update_wrapper(self, func) self.func = func def __call__(self, *args, **kwargs): val = self.func(*args, **kwargs) return "" + val + "" @Emphasis def hello(name): return f"Hello {name}" print(hello("Nafi")) print(hello("Redowan"))>>> Hello Nafi Hello RedowanМетод
__init()__
сохраняет ссылку на функциюnum_calls
и может выполнять другой необходимый код инициализации. Метод__call()__
будет вызываться вместо функции, к которой мы хотим применять декоратор. По сути, он делает то же самое, что и функцияwrapper()
из наших предыдущих примеров. Обратите внимание, что в этом примере используется функцияfunctools.update_wrapper()
вместо@functools.wraps
.Прежде чем мы двигаться дальше, давайте определим декоратор с сохранением состояния, используя синтаксис класса. Декораторы с сохранением состояния могут запоминать его некоторое состояние от предыдущего запуска. В примере ниже декоратор
Tally
с сохранением данных состояния в словаре, который следит за тем, сколько раз были вызваны декорируемые функции. Ключи этого словаряtally
будут содержать имена соответствующих функций, а значения – количество их вызовов.import functools class Tally: def __init__(self, func): functools.update_wrapper(self, func) self.func = func self.tally = {} self.n_calls = 0 def __call__(self, *args, **kwargs): self.n_calls += 1 self.tally[self.func.__name__] = self.n_calls print("Callable Tally:", self.tally) return self.func(*args, **kwargs) @Tally def hello(name): return f"Hello {name}!" print(hello("Redowan")) print(hello("Nafi"))>>> Callable Tally: {'hello': 1} Hello Redowan! Callable Tally: {'hello': 2} Hello Nafi!Еще несколько примеров
Кэширование возвращаемых значений
Декораторы могут предоставить элегантный способ запоминания (кэширования) возвращаемых значений функции.
Это способ оптимизации, при котором сохраняется результат выполнения функции, а затем этот результат используется при следующем вызове, называется мемоизацией (memoization).
Представим себе следующую ситуацию, у нас в приложении используется достаточно ресурсоемкий (или с длительным временем обращения) API, и вы хотели бы, по возможности, как можно реже к нему обращаться. Идея состоит в том, чтобы сохранять и кэшировать значения, возвращаемые вызовами API для конкретных значений параметров запроса. В случае их повторного запроса с помощью API с указанными аргументами, вы могли бы просто сразу возвращать результаты из кэша вместо совершения повторного вызова методов API. Этот прием может значительно улучшить производительность вашего приложения. В примере кода ниже я смоделировал “дорогой” вызов API с использованием модуля time.
import functools import time def api(a): """API принимает в качестве параметра целое число и возвращает его квадрат. Для имитации времени работы процесса обращения я добавил временную задержку.""" print("The API has been called...") # Сделаем задержку 3 сек time.sleep(3) return a * a api(3)>>> The API has been called... 9И так запуск и выполнение функции
api()
занимает примерно 3 секунды. Для того, чтобы кэшировать результат ее выполнения, мы можем использовать функциюfunctools.lru_cache
. С ее помощью мы можем сохранить результат выполнения функцииapi()
в словаре, а затем использовать его, когда снова будет необходим запрос к API с тем же параметром. При этом словарь, содержащий информацию о прошлых запросах к API, будет иметь следующий вид: в качестве ключей будет использоваться значение параметра запроса, а в качестве значения, соответствующего ключу – ответ API (результат выполнения функции). Единственным недостатком этого способа является то, что параметры (аргументы) запроса к API должны иметь такой вид, что могут быть легко преобразованы в хэш, а точнее корректное наименование ключа словаря.import functools @functools.lru_cache(maxsize=32) def api(a): """API принимает в качестве параметра целое число и возвращает его квадрат. Для имитации времени работы процесса обращения я добавил временную задержку.""" print("The API has been called...") # This will delay 3 seconds time.sleep(3) return a * a api(3)>>> 9Особенностью технической реализации метода
functools.lru_cache()
является принцип хранения полученных ранее результатов Least Recently Used LRU, что подразумевает организацию элементов словаря в порядке их использования, что позволяет быстро определить, какой элемент не использовался в течение длительного времени.Least recently used (вытеснение давно неиспользуемых). Из словаря с данными запросов и ответов в первую очередь, вытесняется (убираются) не использующиеся дольше всех данные. Этот алгоритм требует отслеживания того, что и когда использовалось при работе кода.
В примере кода выше параметр метода
max_size
определяет максимальное число возвращаемых значений, которые могут быть сохранены до того, как он начнет удалять самые ранние из них. И теперь если мы запустим функциюapi()
на выполнение, то увидим, что первый раз для возврата ее результата потребуется около 3 секунд. Но если вы снова запустите ее с тем же параметром, то она почти мгновенно вернет результат из своего кэша.Преобразование единиц измерения
Следующий декоратор, который мы рассмотрим преобразует длину из единиц измерения СИ в единицы измерения других систем, не загрязняя целевую декорируемую функцию логикой преобразования.
import functools def convert(func=None, convert_to=None): """Этот код конвертирует единицы измерения из одного типа в другой.""" if func is None: return functools.partial(convert, convert_to=convert_to) @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Conversion unit: {convert_to}") val = func(*args, **kwargs) # Добавим правила для преобразования if convert_to is None: return val elif convert_to == "km": return val / 1000 elif convert_to == "mile": return val * 0.000621371 elif convert_to == "cm": return val * 100 elif convert_to == "mm": return val * 1000 else: raise ValueError("Conversion unit is not supported.") # этот тип единиц не поддерживается return wrapperДавайте используем этот пример кода и применим этот декоратор для функции, которая рассчитывает площадь прямоугольника.
@convert(convert_to="mile") def area(a, b): return a * b area(1, 2)>>> Conversion unit: mile 0.001242742Полученный результат вызова функции расчета площади, к которой мы применили наш декоратор, показывает, что она не только рассчитывает правильное значение, но и выводит единицу длинны в которую мы преобразовывали входные значения. Поэкспериментируйте с другими единицами измерения и посмотрите, что получится.
Регистрация функции для логирования выполнения кода
Ниже приведен пример регистрации функции логирования в фреймворке Flask. Декоратор
register_logger
не вносит никаких изменений в декорируемую функциюlogger
. Он берет функцию и регистрирует ее в спискеlogger_list
каждый раз, когда она вызывается.from flask import Flask, request app = Flask(__name__) logger_list = [] def register_logger(func): logger_list.append(func) return func def run_loggers(request): for logger in logger_list: logger(request) @register_logger def logger(request): print(request.method, request.path) @app.route("/") def index(): run_loggers(request) return "Hello World!" if __name__ == "__main__": app.run(host="localhost", port="5000")Если вы запустите локальный или другой, который вы используете при разработке, сервер и отправите ему запрос со следующим url
http://localhost:5000/
, то он поприветствует вас сообщениемHello World!
. Также будет выведено тип http-метода запросаmethod
и его относительный путьpath
. Более того, если вы просмотрите содержимое спискаlogger_list
, то найдете там наш зарегистрированный логгер.Самостоятельно ознакомившись с исходным кодом фреймворка Flask, вы найдете намного больше примеров реального использования декораторов.
Небольшое дополнение
Все фрагменты кода из этой статьи были разработаны и протестированы на компьютере с Ubuntu 18.04 и Python 3.8.