Python: Декораторы

Всё, с чем мы работаем в Python — это объекты в терминах ООП: переменные, классы, экземпляры классов и даже импортированные модули. И функции — не исключение.
Запустите в консоли интерактивный режим Python, нам надо поэкспериментировать.
Выполните такой код:
Скопировать кодPYTHON
def func(x, y): return x+y print(type(func)) # результат: <class 'function'>
При создании нового объекта он сохраняется в памяти по определённому адресу, а имя добавляется в доступное пространство имен и указывает на этот адрес.
Скопировать кодPYTHON
# посмотрим, по какому адресу в памяти сохранена функция func() hex(id(func)) # результат: '0x21115c49280' # встроенная функция id() показывает адрес, по которому в памяти сохранён объект # а встроенная функция hex() выводит этот адрес в принятом для таких задач # шестнадцатеричном формате
Создадим переменную new_func и присвоим ей значение func, точно так же, как присвоили бы переменной строковый или числовой объект:
Скопировать кодPYTHON
new_func = func print(new_func(1, 2)) # результат: 3 # и посмотрим, на какой адрес указывает имя new_func hex(id(new_func)) # '0x21115c49280' # это тот же адрес, на который указывает и имя func: # два указателя направляют на один адрес
Значение первой переменной можно заменить, но вторая переменная всё равно будет указывать на прежний объект.
Скопировать кодPYTHON
# переопределим переменную func, теперь она примет объект типа str func = 'just a string' print(func) # результат: just a string # посмотрим, на какой адрес теперь указывет имя func hex(id(func)) # результат: '0x7ffba34096a0' # адрес другой: был создан новый объект типа str, он сохранён по новому адресу # и имя func указывает на него # но переданное в new_func значение сохранилось: # new_func всё ещё указывает на на прежний адрес, где сохранена функция hex(id(new_func)) # результат: '0x21115c49280'
Старую переменную func мы переопределили, но new_func продолжает указывать на ту же самую функцию, на которую изначально указывало имя func.
Поскольку функция ведет себя как обычная переменная, то ее можно передавать в качестве параметра в другую функцию:
Скопировать кодPYTHON
def apply(f, x, y): return f(x, y) print(apply(new_func, 1, 2)) # результат: 3
Если функцию можно передать, то что мешает вернуть функцию?
Скопировать кодPYTHON
def operation(name): def add(x, y): return x + y def mul(x, y): return x * y if name == 'add': return add if name == 'mul': return mul # сложение op = operation('add') print(op(1, 3)) # результат: 4 # умножение op2 = operation('mul') print(op2(2, 5)) # результат: 10 # или даже так: умножение, но без промежуточной функции print(operation('mul')(2, 5)) # результат: 10
Внутри функции мы создали другие функции, а потом внешним переменным присвоили ссылки на эти «внутренние» функции.
Можно сделать еще один шаг: обернуть входящую функцию нужным кодом прежде, чем её исполнить.
Скопировать кодPYTHON
def wrapper(func): # какие-то действия с func _cache = {'counter': 0} def added_value(): _cache['counter'] = _cache['counter'] + 1 print("Полезная работа до начала работы функции") func() print("Полезная работа после выполнения функции") return added_value def some_func(): print("Я полезная функция") do = wrapper(some_func) do() # результат: # Полезная работа до начала работы функции # Я полезная функция # Полезная работа после выполнения функции
Можно упростить вызов, не создавая промежуточных переменных, а заменить some_func на саму себя, «обернутую» в функцию wrapper():
Скопировать кодPYTHON
some_func = wrapper(some_func) some_func() # результат: # Полезная работа до начала работы функции # Я полезная функция # Полезная работа после выполнени функции
Теперь при каждом обращении к функции some_func() будет выполняться внутренняя функция added_value() из функции wrapper()
Код, размещённый в декораторе вне внутренней функции, будет выполнен лишь единожды. В декораторе wrapper() будет создана переменная _cache и значение _cache['counter'] будет увеличиваться на единицу с каждым вызовом декоратора, но не будет создаваться каждый раз заново.
Задача по изменению работы функций достаточно распространена. Например, можно «обернуть» функцию и измерить время ее выполнения. Или зарегистрировать функцию в реестре, чтобы потом обращаться к ней через этот реестр: мы делали так при создании странного фильтра uglify.
Можно одной и той же «обёрткой» настроить разные функции так, чтобы они выполнялись только один раз, сохраняли значение и при повторном обращении моментально отдавали готовый результат.
Такое решение оказалось столь популярно и удобно, что в языке была добавлена специальная конструкция: декоратор.
Если перед определением функции написать имя «оборачивающей» функции через знак @, то определяемая функция будет передаваться в «обёртку» и возвращать результат через неё.
Скопировать кодPYTHON
@wrapper # оборачиваем some_func() в декоратор wrapper() def some_func(): print("Я полезная функция")
Упрощённый синтаксис, «синтаксический сахар», существует только для того, чтобы делать работу программиста удобнее.

Работа с аргументами

Функция-декоратор получает на вход только один параметр — декорируемую функцию, а возвращает внутреннюю функцию-исполнитель. Параметры декорируемой функции можно передать в функцию-исполнитель.
Декораторы обычно пишут универсальными: они должны принимать на вход функции с любым количеством и типом параметров. Для этого есть конструкции *args и **kwargs, это маски для получения любого количества позиционных (*args от arguments) и именованных (**kwargs от keyword arguments) аргументов. Эти конструкции могут применяться в любых функциях (не только в декораторах), имена arg и kwarg не предустановлены, но общеприняты.
В теле функции с переменной *args можно работать как с кортежем, а с переменной **kwargs — как со словарём.
Скопировать кодPYTHON
# функция-обертка def wrapper(func): # какие-то действия с func # функция-исполнитель # получает аргументы из декорируемой функции def added_value(*args, **kwargs): print("Полезная работа до начала работы функции") func(*args, **kwargs) # напечатаем аргументы print (args) print (kwargs) print("Полезная работа после выполнени функции") return added_value @wrapper # декоратор def some_func(): # декорируемая функция # код функции
Теперь любая декорируемая функция будет вызвана без ошибок.