Декораторы
Для того, чтобы понять, как работают декораторы, в первую очередь следует вспомнить, что функции в Python - тоже объекты, соответственно, их можно возвращать из другой функции или передавать в качестве аргумента. Также следует помнить, что функция в python может быть определена и внутри другой функции.
Вспомнив это, можно смело переходить к декораторам. Декораторы — это, по сути, "обёртки", которые дают нам возможность изменить поведение функции, не изменяя её код.
Создадим свой декоратор:
def my_shiny_new_decorator(function_to_decorate): def the_wrapper_around_the_original_function(): print("Я - код, который отработает до вызова функции") function_to_decorate() print("А я - код, срабатывающий после") return the_wrapper_around_the_original_function
Внутри себя декоратор определяет функцию-"обёртку". Она будет обёрнута вокруг декорируемой, получая возможность исполнять произвольный код до и после неё.
Представим теперь, что у нас есть функция, которую мы не планируем больше трогать.
def stand_alone_function(): print("Я простая одинокая функция, ты ведь не посмеешь меня изменять?")
Однако, чтобы изменить её поведение, мы можем "декорировать" её, то есть просто передать объекту, который обернёт исходную функцию в любой код, который нам потребуется, и вернёт уже новую, готовую к использованию функцию:
stand_alone_function_decorated = my_shiny_new_decorator(stand_alone_function) stand_alone_function_decorated()
Я - код, который отработает до вызова функции Я простая одинокая функция, ты ведь не посмеешь меня изменять? А я - код, срабатывающий после
Теперь мы бы хотели, чтобы каждый раз во время вызова stand_alone_function, вместо неё вызывалась stand_alone_function_decorated. Для этого просто перезапишем stand_alone_function:
stand_alone_function = my_shiny_new_decorator(stand_alone_function) stand_alone_function()
Я - код, который отработает до вызова функции Я простая одинокая функция, ты ведь не посмеешь меня изменять? А я - код, срабатывающий после
Собственно, это и есть декораторы. Вот так можно было записать предыдущий пример, используя синтаксис декораторов:
@my_shiny_new_decorator def another_stand_alone_function(): print("Оставь меня в покое") another_stand_alone_function()
Я - код, который отработает до вызова функции Оставь меня в покое А я - код, срабатывающий после
То есть, декораторы в Python — это просто синтаксический сахар для конструкций вида:
another_stand_alone_function = my_shiny_new_decorator(another_stand_alone_function)
При этом, естественно, можно использовать несколько декораторов для одной функции, например так:
def bread(func): def wrapper(): print() func() print("<\______/>") return wrapper def ingredients(func): def wrapper(): print("#помидоры#") func() print("~салат~") return wrapper def sandwich(food="--ветчина--"): print(food) sandwich = bread(ingredients(sandwich)) sandwich()
#помидоры# --ветчина-- ~салат~ <\______/>
и с использованием синтаксиса декораторов:
@bread @ingredients def sandwich(food="--ветчина--"): print(food) sandwich()
#помидоры# --ветчина-- ~салат~ <\______/>
Также нужно помнить о том, что важен порядок декорирования. Сравните с предыдущим примером:
@ingredients @bread def sandwich(food="--ветчина--"): print(food) sandwich()
#помидоры# --ветчина-- <\______/> ~салат~
Передача декоратором аргументов в функцию
Однако, все декораторы, которые мы рассматривали, не имели одного очень важного функционала — передачи аргументов декорируемой функции. Собственно, это тоже несложно сделать.
def a_decorator_passing_arguments(function_to_decorate): def a_wrapper_accepting_arguments(arg1, arg2): print("Смотри, что я получил:", arg1, arg2) function_to_decorate(arg1, arg2) return a_wrapper_accepting_arguments
Теперь, когда мы вызываем функцию, которую возвращает декоратор, мы вызываем её "обёртку", передаём ей аргументы и уже в свою очередь она передаёт их декорируемой функции
@a_decorator_passing_arguments def print_full_name(first_name, last_name): print("Меня зовут", first_name, last_name) print_full_name("Vasya", "Pupkin") # Смотри, что я получил: Vasya Pupkin # Меня зовут Vasya Pupkin
Декорирование методов
Один из важных фактов, которые следует понимать, заключается в том, что функции и методы в Python — это практически одно и то же, за исключением того, что методы всегда ожидают первым параметром ссылку на сам объект self. Это значит, что мы можем создавать декораторы для методов точно так же, как и для функций, просто не забывая про self.
def method_friendly_decorator(method_to_decorate): def wrapper(self, lie): lie -= 4 return method_to_decorate(self, lie) return wrapper class Lucy: def __init__(self): self.age = 32 @method_friendly_decorator def say_age(self, lie): print(f"Мне {self.age + lie} лет, а ты бы сколько дал?") l = Lucy() l.say_age(-3)
Выведет Мне 25 лет, а ты бы сколько дал? (32 + (-3 - 5))
Конечно, если мы создаём максимально общий декоратор и хотим, чтобы его можно было применить к любой функции или методу, то можно воспользоваться распаковкой аргументов:
def a_decorator_passing_arbitrary_arguments(function_to_decorate): # Данная "обёртка" принимает любые аргументы def a_wrapper_accepting_arbitrary_arguments(*args, **kwargs): print("Передали ли мне что-нибудь?:") print(args) print(kwargs) function_to_decorate(*args, **kwargs) return a_wrapper_accepting_arbitrary_arguments class Mary(object): def __init__(self): self.age = 31 @a_decorator_passing_arbitrary_arguments def say_age(self, lie=-3): # Теперь мы можем указать значение по умолчанию print("Мне {} лет, а ты бы сколько дал?".format(self.age + lie)) m = Mary() m.say_age()
Передали ли мне что-нибудь?: (<__main__.Mary object at 0x000002371D4E91F0>,) {} Мне 28 лет, а ты бы сколько дал?
Декораторы с аргументами
А теперь попробуем написать декоратор, принимающий аргументы:
>>> def decorator_maker(): ... print("Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать декоратор.") ... def my_decorator(func): ... print("Я - декоратор! Я буду вызван только раз: в момент декорирования функции.") ... def wrapped(): ... print ("Я - обёртка вокруг декорируемой функции.\n" ... "Я буду вызвана каждый раз, когда ты вызываешь декорируемую функцию.\n" ... "Я возвращаю результат работы декорируемой функции.") ... return func() ... print("Я возвращаю обёрнутую функцию.") ... return wrapped ... print("Я возвращаю декоратор.") ... return my_decorator ... >>> # Давайте теперь создадим декоратор. Это всего лишь ещё один вызов функции >>> new_decorator = decorator_maker() Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать декоратор. Я возвращаю декоратор. >>> >>> # Теперь декорируем функцию >>> def decorated_function(): ... print("Я - декорируемая функция.") ... >>> decorated_function = new_decorator(decorated_function) Я - декоратор! Я буду вызван только раз: в момент декорирования функции. Я возвращаю обёрнутую функцию. >>> # Теперь наконец вызовем функцию: >>> decorated_function() Я - обёртка вокруг декорируемой функции. Я буду вызвана каждый раз, когда ты вызываешь декорируемую функцию. Я возвращаю результат работы декорируемой функции. Я - декорируемая функция.
Теперь перепишем данный код с помощью декораторов:
>>> @decorator_maker() ... def decorated_function(): ... print("Я - декорируемая функция.") ... Я создаю декораторы! Я буду вызван только раз: когда ты попросишь меня создать декоратор. Я возвращаю декоратор. Я - декоратор! Я буду вызван только раз: в момент декорирования функции. Я возвращаю обёрнутую функцию. >>> decorated_function() Я - обёртка вокруг декорируемой функции. Я буду вызвана каждый раз когда ты вызываешь декорируемую функцию. Я возвращаю результат работы декорируемой функции. Я - декорируемая функция.
Вернёмся к аргументам декораторов, ведь, если мы используем функцию, чтобы создавать декораторы "на лету", мы можем передавать ей любые аргументы, верно?
>>> def decorator_maker_with_arguments(decorator_arg1, decorator_arg2): ... print("Я создаю декораторы! И я получил следующие аргументы:", ... decorator_arg1, decorator_arg2) ... def my_decorator(func): ... print("Я - декоратор. И ты всё же смог передать мне эти аргументы:", ... decorator_arg1, decorator_arg2) ... # Не перепутайте аргументы декораторов с аргументами функций! ... def wrapped(function_arg1, function_arg2): ... print ("Я - обёртка вокруг декорируемой функции.\n" ... "И я имею доступ ко всем аргументам\n" ... "\t- и декоратора: {0} {1}\n" ... "\t- и функции: {2} {3}\n" ... "Теперь я могу передать нужные аргументы дальше" ... .format(decorator_arg1, decorator_arg2, ... function_arg1, function_arg2)) ... return func(function_arg1, function_arg2) ... return wrapped ... return my_decorator ... >>> @decorator_maker_with_arguments("Леонард", "Шелдон") ... def decorated_function_with_arguments(function_arg1, function_arg2): ... print ("Я - декорируемая функция и я знаю только о своих аргументах: {0}" ... " {1}".format(function_arg1, function_arg2)) ... Я создаю декораторы! И я получил следующие аргументы: Леонард Шелдон Я - декоратор. И ты всё же смог передать мне эти аргументы: Леонард Шелдон >>> decorated_function_with_arguments("Раджеш", "Говард") Я - обёртка вокруг декорируемой функции. И я имею доступ ко всем аргументам - и декоратора: Леонард Шелдон - и функции: Раджеш Говард Теперь я могу передать нужные аргументы дальше Я - декорируемая функция и я знаю только о своих аргументах: Раджеш Говард
Таким образом, мы можем передавать декоратору любые аргументы, как обычной функции. Мы можем использовать и распаковку через *args и **kwargs в случае необходимости.
Некоторые особенности работы с декораторами
- Декораторы несколько замедляют вызов функции, не забывайте об этом
- Вы не можете "раздекорировать" функцию. Безусловно, существуют трюки, позволяющие создать декоратор, который можно отсоединить от функции, но это плохая практика. Правильнее будет запомнить, что если функция декорирована — это не отменить
- Декораторы оборачивают функции, что может затруднить отладку
Последняя проблема частично решена добавлением в модуле functools функции functools.wraps, копирующей всю информацию об оборачиваемой функции (её имя, из какого она модуля, её документацию и т.п.) в функцию-обёртку.
Забавным фактом является то, что functools.wraps тоже является декоратором.
>>> def foo(): ... print("foo") ... >>> print(foo.__name__) foo >>> # Однако, декораторы мешают нормальному ходу дел: ... def bar(func): ... def wrapper(): ... print("bar") ... return func() ... return wrapper ... >>> @bar ... def foo(): ... print("foo") ... >>> print(foo.__name__) wrapper >>> import functools # "functools" может нам с этим помочь >>> def bar(func): ... # Объявляем "wrapper" оборачивающим "func" ... # и запускаем магию: ... @functools.wraps(func) ... def wrapper(): ... print("bar") ... return func() ... return wrapper ... >>> @bar ... def foo(): ... print("foo") ... >>> print(foo.__name__) foo