Уважаемые читатели, рад вас приветствовать в новой статье. Этот материал является продолжением предыдущей публикации, посвященной замыканиям. В данной части обзора мы рассмотрим декораторы.
Эта статья написана в первую очередь для тех, кто только начинает свой путь в программировании или начал изучать Python. Потому здесь я не буду рассматривать декораторы классов, чтобы сделать материал более доступным для новичков. Тем не менее, для тех, кто изучит данную статью, не составит труда разобраться в декораторах классов, так как они не имеют существенных отличий от рассматриваемых здесь декораторов функций.
Продолжая свой рассказ, я предполагаю, что вы уже ознакомились с предыдущей частью, посвященной замыканиям. Хоть эта статья и сфокусирована на декораторах, без знания концепции замыканий будет сложно разобраться с представленным материалом.
Введение в декораторы
В Python декораторы представляют собой мощный инструмент, который позволяет модифицировать поведение функций без изменения их кода. Чаще всего они используются для добавления дополнительной функциональности вокруг существующих функций. Это может включать в себя не только изменение выходных данных или выполнения определенных действий до и после вызова функции, но также возможность комбинирования нескольких декораторов для создания сложной функциональности. Кроме того, декораторы используются для логирования и отладки, обеспечивая прозрачный и эффективный способ отслеживания выполнения кода и выявления возможных проблем. Таким образом, декораторы становятся важным инструментом для улучшения гибкости и эффективности кода в Python.
В широком смысле, декораторы в Python это вызываемые объекты, которые создаются с помощью замыканий, принимают в качестве аргумента и возвращают дополненный или измененный вызываемый объект. В отношении декораторов функций, определение можно сформулировать следующим образом: декоратор это функция, которая создается с помощью замыкания, принимающая в качестве аргумента оборачиваемую функцию и возвращающая обернутую функцию.
Рассмотрим простой пример
def decorator(foo):
def wrapper():
print('text before foo call')
foo()
print('text after foo call')
return wrapper
def greet():
print('Hello')
greet() # Hello
# Применяем декоратор к функции greet
greet = decorator(greet)
greet() # text before foo call
# Hello
# text after foo call
В данном примере видно, что до декорирования функция выполняет ожидаемое действие, но после декорирования результат ее вызова меняется, хотя код самой функции не менялся. Это и есть изменение (модификация) поведения.
В Python реализован синтаксический сахар позволяющий декорировать функции проще, чем на предыдущем примере. Для этого перед оборачиваемой функцией необходимо указать имя функции-декоратора, добавив перед ним @
при этом не требуется записывать в скобках имя декорируемой функции, т.к. интерпретатор сам понимает, что декорируется функция из следующей строки.
Предыдущий пример с учетом данного синтаксиса будет выглядеть так:
def decorator(foo):
def wrapper():
print('text before foo call')
foo()
print('text after foo call')
return wrapper
@decorator
def greet():
print('Hello')
greet() # text before foo call
# Hello
# text after foo call
Использование декораторной нотации делает код более ясным и удобным для чтения.
К одной функции можно применить несколько декораторов. В развернутом виде, запись будет выглядеть так:
def each_pheasant(foo):
def wrapper():
print('каждый')
foo()
print('фазан')
return wrapper
def hunter_is_sitting(foo):
def wrapper():
print('охотник')
foo()
print('сидит')
return wrapper
def wishes_where(foo):
def wrapper():
print('желает')
foo()
print('где')
return wrapper
def know():
print('знать')
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
know()
# каждый
# охотник
# желает
# знать
# где
# сидит
# фазан
Результат выполнения данного кода будет аналогичен синтаксису:
@each_pheasant
@hunter_is_sitting
@wishes_where
def know():
print('знать')
know()
# каждый
# охотник
# желает
# знать
# где
# сидит
# фазан
В обоих случаях интерпретатор получает новую функцию, обернув предыдущую функцию в обертку, из чего следует, что порядок применения декораторов влияет на результат.
При этом в случае использования синтаксического сахара функция know оборачивается сначала ближайшим декоратором, функция, получившаяся в результате, оборачивается следующим декоратором и так пока не будут применены все декораторы.
Использование декораторов в ином порядке приведет к неверному результату:
@wishes_where
@hunter_is_sitting
@each_pheasant
def know():
print('знать')
know()
# желает
# охотник
# каждый
# знать
# фазан
# сидит
# где
Декораторы с аргументами
Может возникнуть ситуация, в которой декоратору требуется передать какое-то значение в виде аргумента. Решение может показаться не очевидным, если не разобраться с пошаговым оборачиванием функции.
Допустим, у нас есть сложная функция (longrunningfoo) выполнение которой занимает продолжительное время. Но мы заметили, что в нашем сценарии чаще всего при ее вызове используются одни и те же значения (которые впрочем, могут со временем измениться). Для оптимизации работы приложения, мы можем написать декоратор, который будет кэшировать результаты выполнения этой функции. При этом очевидно, что кол-во кэшированных значений должно быть ограничено, иначе производительность будет снижаться уже из-за объема кэша.
В первую очередь создадим функцию с параметром, который будет указывать кол-во значений в кэше.def memoize(amount):
Далее мы создаем функцию-декоратор, которая принимает оборачиваемую функцию в качестве аргументаdef inner(foo):
а уже внутри нее мы создаем функцию-оберткуdef wrapper(arg):
и описываем логику работы декоратора, условие проверяющее, есть ли у нас уже результат для текущего аргумента и условие проверяющее, достигнут ли лимит значений в кэше. После этого собираем все вместе. Получившаяся функция может иметь следующий вид:
def memoize(amount):
args_list = []
memoize_dict = {}
def inner(foo):
def wrapper(arg):
if arg not in memoize_dict:
if len(args_list) == amount:
value = args_list.pop(0)
memoize_dict.pop(value)
args_list.append(arg)
new_value = foo(arg)
memoize_dict[arg] = new_value
return memoize_dict[arg]
return wrapper
return inner
Далее используем присваивание, что бы обернуть декорируемую функцию:
decorator = memoize(3)
long_running_foo = decorator(long_running_foo)
В первой строке мы вызываем функцию memoize
с аргументом 3
(кол-во значений в кэше), которая возвращает нам замыкание inner
, которое и будет нашим декоратором. Его мы присваиваем переменной decorator
. Функция inner
ожидает в качестве аргумента оборачиваемую функцию. Соответственно, мы вызываем получившуюся функцию и передаем ей в качестве аргумента функцию long_running_foo
, после чего обернутую функцию присваиваем переменной с тем же именем. Данный синтаксис можно сократить до одной строки следующим образом:
long_running_foo = memoize(3)(long_running_foo)
В данной строке происходит все то же самое, но без дополнительной переменной. Ну и переходя к использованию синтаксического сахара, вспоминаем, что имя декорируемой функции, в скобках после имени декоратора, писать не нужно, получаем следующую запись
@memoize(3)
Полностью скрипт может выглядеть следующим образом:
def memoize(amount):
args_list = []
memoize_dict = {}
def inner(foo):
def wrapper(arg):
if arg not in memoize_dict:
if len(args_list) == amount:
value = args_list.pop(0)
memoize_dict.pop(value)
args_list.append(arg)
new_value = foo(arg)
memoize_dict[arg] = new_value
return memoize_dict[arg]
return wrapper
return inner
@memoize(3)
def long_running_foo(arg):
print('processing..', end=' ')
return arg
print(long_running_foo(1)) # processing.. 1
print(long_running_foo(2)) # processing.. 2
print(long_running_foo(3)) # processing.. 3
print(long_running_foo(2)) # 2
print(long_running_foo(3)) # 3
print(long_running_foo(4)) # processing.. 4
print(long_running_foo(3)) # 3
print(long_running_foo(1)) # processing.. 1
Декоратор кэширует возвращаемые функцией значения и в случае, если для какого-то аргумента есть результат, возвращает его не вызывая саму функцию.
Сохранение атрибутов при декорировании функции
У любой объявленной функции есть множество атрибутов, начиная с имени, имени модуля и docstring'a, и заканчивая пользовательскими атрибутами. Получить доступ с этим атрибутам можно по соответствующим именам.
def print_greet(name):
"""Some description"""
print('Hello, ' + name + '!')
print(print_greet.__name__) # print_greet
print(print_greet.__doc__) # Some description
Однако, при вызове задекорированной функции, вызывается именно обертка, и при попытке получить какие-либо атрибуты, будут получены атрибуты именно обертки:
def decorator(foo):
def wrapper(*args):
foo(*args)
return wrapper
@decorator
def print_greet(name):
"""Some description"""
print('Hello, ' + name + '!')
print(print_greet.__name__) # wrapper
print(print_greet.__doc__) # None
Мы можем изменить и это поведение, передав функции-обертке соответствующие атрибуты оборачиваемой функции. Для этого после определения функции обертки нужно добавить строки типа wrapper.__name__ = foo.__name__
.
def decorator(foo):
def wrapper(*args):
foo(*args)
wrapper.__name__ = foo.__name__
wrapper.__doc__ = foo.__doc__
return wrapper
@decorator
def print_greet(name):
"""Some description"""
print('Hello, ' + name + '!')
print(print_greet.__name__) # print_greet
print(print_greet.__doc__) # Some description
Очевидным минусом этого решения является необходимость добавления отдельной строки для каждого атрибута, что захламляет код. Лучшей практикой в данной ситуации считается использование декоратора wraps
из модуля functools
. С помощью этого декоратора нужно декорировать функцию-обертку (иронично) и передать ему в качестве аргумента оборачиваемую функцию. В данном примере это будет выглядеть следующим образом:
from functools import wraps
def decorator(foo):
@wraps(foo)
def wrapper(*args):
foo(*args)
return wrapper
@decorator
def print_greet(name):
"""Some description"""
print('Hello, ' + name + '!')
print(print_greet.__name__) # print_greet
print(print_greet.__doc__) # Some description
Однако следует отметить, что декоратор wraps копирует не все атрибуты, а только основные. Подробнее в документации
Несколько популярных декораторов из библиотеки functools
Далее я перечислю несколько декораторов и дам небольшое описание к каждому. Я не буду приводить примеры, но рекомендую читателю самостоятельно изучить документацию к ним, их код и поэкспериментировать с их применением.
@functools.lru_cache: Кэширует результаты вызова функции с использованием алгоритма "Least Recently Used" (LRU). Это позволяет ускорить выполнение функций с дорогостоящими вычислениями.
@functools.partial: Частично применяет функцию, фиксируя некоторые ее аргументы. Это позволяет создавать новые функции с уменьшенным набором параметров.
@functools.cache (добавлен в Python 3.9): Кэширует результаты вызова функции с возможностью настройки параметров кэширования.
@functools.deprecated (добавлен в Python 3.9): Помечает функцию как устаревшую, что предупреждает пользователей о том, что функция будет удалена в будущих версиях.
Больше декораторов с описаниями и примерами доступны по ссылке
Раздекорирование
В контексте использования декораторов порой возникает вопрос о том, можно ли вернуть функцию к ее начальному состоянию. И хорошая рекомендация была дана в другой статье о декораторах:
Вы не можете «раздекорировать» функцию. Безусловно, существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика. Правильней будет запомнить, что если функция декорирована — это не отменить.
Я с этой позицией полностью согласен. Тем не менее, в комментариях к первой части этот вопрос так же обсуждался и мне кажется, что знание "как это работает" может быть полезным для более глубокого понимания вопроса.
Как уже было сказано выше, лучшей практикой при написании декораторов является использование декоратора wraps
. Кроме прочего, данный декоратор добавляет к обернутой функции атрибут __wrapped__
, который содержит в себе ссылку на оборачиваемую функцию.
Воспользуемся одним из предыдущих примеров, переписав его с использованием декоратора wraps
from functools import wraps
def each_pheasant(foo):
@wraps(foo)
def wrapper():
print('каждый')
foo()
print('фазан')
return wrapper
def hunter_is_sitting(foo):
@wraps(foo)
def wrapper():
print('охотник')
foo()
print('сидит')
return wrapper
def wishes_where(foo):
@wraps(foo)
def wrapper():
print('желает')
foo()
print('где')
return wrapper
def know():
print('знать')
Далее, мы можем передать функцию know функции print в качестве аргумента, не вызывая. В результате мы получим информацию о самой функции, включая её местоположение в памяти.
print(know) # <function know at 0x7fa16ccab130>
Далее, мы декорируем функцию, через присваивание, без использования синтаксического сахара, и снова получаем информацию о ней.
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know) # <function know at 0x7fa16ccab2e0>
Очевидно, что адрес в памяти изменился, хотя название функции осталось прежним. Как уже было сказано выше, мы получаем адрес обертки.
У большинства объектов в python, за исключением встроенных типов данных, есть атрибут __dict__
содержащий в себе словарь с атрибутами этого объекта. Мы можем получить доступ к этому словарю, обратившись к соответствующему атрибуту.
print(know.__dict__) # {'__wrapped__': <function know at 0x7fde13b23250>}
В данном примере, атрибут __wrapped__
был создан именно декоратором wraps
. При этом другие декораторы могут создавать другие атрибуты. Например, упоминавшийся выше, lru_cache
добавляет 7 атрибутов.
С помощью присваивания, мы можем получить доступ к оборачиваемой функции - развернуть функцию или раздекорировать.
function_without_first_wrapper = know.__wrapped__
После этого мы можем даже вызвать получившуюся функцию.
function_without_first_wrapper()
# охотник
# желает
# знать
# где
# сидит
Повторив ту же операцию несколько раз, мы можем получить переменную, которая будет ссылаться на оригинальную функцию.
# получаем еще одну переменную, ссылающуюся на не обернутую функцию
original_know = know
print(know) # <function know at 0x7fa16ccab130>
# оборачиваем функцию
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know) # <function know at 0x7fa16ccab2e0>
# "разворачиваем" функцию, получая ссылку на оборачиваемую функцию
function_without_first_wrapper = know.__wrapped__
function_without_second_wrapper = function_without_first_wrapper.__wrapped__
function_without_third_wrapper = function_without_second_wrapper.__wrapped__
# проверяем, что "развернутая" функция ссылается на тот же объект, что и "оригинальная"
print(function_without_third_wrapper is original_know) # True
Как мы видим в итоге, мы получили переменную function_without_third_wrapper
ссылающуюся на тот же объект в памяти, что и original_know
Весь код
from functools import wraps
def each_pheasant(foo):
@wraps(foo)
def wrapper():
print('каждый')
foo()
print('фазан')
return wrapper
def hunter_is_sitting(foo):
@wraps(foo)
def wrapper():
print('охотник')
foo()
print('сидит')
return wrapper
def wishes_where(foo):
@wraps(foo)
def wrapper():
print('желает')
foo()
print('где')
return wrapper
def know():
print('знать')
# получаем еще одну переменную, ссылающуюся на не обернутую функцию
original_know = know
print(know) # <function know at 0x7fa16ccab130>
# оборачиваем функцию
know = each_pheasant(hunter_is_sitting(wishes_where(know)))
print(know) # <function know at 0x7fa16ccab2e0>
# "разворачиваем" функцию, получая ссылку на оборачиваемую функцию
function_without_first_wrapper = know.__wrapped__
function_without_second_wrapper = function_without_first_wrapper.__wrapped__
function_without_third_wrapper = function_without_second_wrapper.__wrapped__
# проверяем, что "развернутая" функция ссылается на тот же объект, что и "оригинальная"
print(function_without_third_wrapper is original_know) # True
WhiteApfel в упомянутом обсуждении предложил функцию, которая принимает декорированную функцию и пытается извлечь из нее оригинальную функцию, просматривая замыкания (closures) функции и ищет функции, которые не содержат в своем замыкании других функций. Вы можете ознакомиться со скриптом, перейдя по ссылке.
Завершая тему, могу порекомендовать отказаться от использования синтаксического сахара и воспользоваться декорированием через присваивание в случае, когда может понадобиться доступ к не декорированной функции:
decored_foo = decorator(foo)
Заключение
В данной статье мы рассмотрели различные аспекты использования декораторов в Python, начиная с их основ и заканчивая более сложными концепциями, такими как раздекорирование функций. Узнали, как декораторы позволяют модифицировать поведение функций, добавляя дополнительную функциональность вокруг существующих функций и как они могут использоваться для оптимизации работы приложений.
Важно помнить, что декораторы представляют собой мощный инструмент, который может значительно улучшить гибкость и эффективность вашего кода. Однако необходимо использовать их с умом и пониманием их работы, чтобы избежать потенциальных проблем.
Надеюсь, что данная статья помогла вам лучше понять концепцию декораторов и их применение в Python. Не забывайте экспериментировать с кодом и применять новые знания на практике!
Комментарии (4)
SwetlanaF
14.03.2024 13:34+1Большое спасибо за статью! Я ее ждала)) Как раз прошла на степике два курса (поколение Пайтон продвинутый P. и Инди P. Артема Егорова). Оч. качественные курсы, всем новичкам рекомендую. Сейчас проверю, как я эту тему поняла.
anonymous
НЛО прилетело и опубликовало эту надпись здесь
vladislav_smirnov Автор
Тут даже не в минусе печаль, а в том, что человек не счел нужным написать, что ему не понравилось
Fox_exe
Скорее всего как обычно - "Ничего нового. Всё написанное легко гуглиться или есть в доках".
Хотя как ещё одна "шпаргалка" вполне имеет место быть.