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

Автор: | 06/08/2015
 

PythonУ Python есть интересная возможность, которая называется «декораторы» (decorators), которая позволяет добавлять функциональность к уже существующему коду. Такая возможность так же называется метапрограммированием — когда одна часть программы пытается модифицировать другую часть во время компиляции.

Введение

Что бы понять суть работы декораторов — мы сначала должны рассмотреть несколько базовых концепций Python. Во-первых — мы должны понимать, что абсолютно всё в Python вляется объектами. Имена, которые мы определяем, являются просто указателями к этим объектам. Функции не являются исключением из этого правила — они тоже являются объектами с атрибутами. Несколько различных имён могут быть связаны с одним и тем же объектом. Например:

>>> def first(msg):
... print(msg)
...

>>> first("Hello")
Hello

>>> second = first
>>> second("Hello")
Hello

Тут имена first и second связаны с одним и тем же объектом функции.

Теперь — рассмотрим более странные вещи. Функции могут передаваться в качестве аргументов другим функциям. Если вы уже использовали такие функции как map(), filter() или reduce() в Python — значит вы уже сталкивались с таким подходом. Функции, которые принимают другие функции в качестве аргументов называются функциями высшего порядка (higher order functions). Вот пример такой функции:

def inc(x):
  """Function to increase value by 1"""
  return x + 1

def dec(x):
  """Function to decrease value by 1"""
  return x - 1

def operate(func, x):
  """A higer order function to increase or decrease"""
  result = func(x)
  return result

Далее — мы можем вызвать функцию operate() так:

>>> operate(inc,3)
4
>>> operate(dec,3)
2

Кроме того — функция может возвращать другую функцию:

>>> def is_called():
...   def is_returned():
...     print("Hello")
...   return is_returned
...

>>> new = is_called()
>>> new()
Hello

Тут функция is_returned() является вложенной функцией, которая инициализируется и возвращается каждый раз, когда вызывается функция is_called().

Кроме того — было полезно бы знать о замыканиях в Python (to be traslated :-)).

Декораторы

Фактически, любой объект, который имеет специальный метод __cal__() является вызываемым. В общем смысле — декораторы являются вызываемыми объектами, которые возвращают другой вызываемый объект. Как правило — декоратор принимает функцию, добавляет некоторую функциональность к ней, и возвращает её:

>>> def make_pretty(func):
...   def inner():
...     print("I got decorated")
...     func()
...   return inner
...
>>> def ordinary():
...   print("I am ordinary")
...
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary

В этом примере make_pretty() является декотратором. Во время присвоения:

pretty = make_pretty(ordinary)

Функция ordinary() становится «декорированной», а функции, которая возвращается из make_pretty() присваивается имя pretty. Мы видим, что декоратор добавил новую функциональность к оригинальной функции ordinary(). Этот процесс схож с упаковкой подарка — декторатор выполняет роль обёртки. Суть поведения объекта, которые подвергся «декорации» (как подарок внутри упаковки) — не меняется.

Обычно мы «декорируем» функцию и присваиваем ей какое-то имя, как в примере выше:

ordinary = make_pretty(ordinary)

Это распростраённое действие, поэтому у Python имеется специальный синтаксис для таких случаев — мы можем использовать символ @ с именем функции-декоторатора, и разместить её перед функцией, которая будет передана декоратору, например — такой код:

>>> @make_pretty
... def ordinary():
...   print("I am ordinary")

Будет обработан аналогично такому:

>>> def ordinary():
...   print("I am ordinary")
...
>>> ordinary = make_pretty(ordinary)

Функции-декоторы с параметрами

Декоратор в предыдущем примере был очень простым, и он сработает для функций, у которых нет парамтеров. Что если у нас будет функция, которая имеет несколько параметров? Например:

>>> def divide(a, b):
...   return a/b

У этой функции есть два параметра — a и b, и мы знаем, что получим ошибку, если передадим 0 в b:

>>> divide(2,5)
0
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: integer division or modulo by zero

Теперь — давайте создадим декоратор, который будет проверять условие на наличие недопустимого действия:

>>> def smart_divide(func):
...   def inner(a,b):
...     print("I am going to divide",a,"and",b)
...     if b == 0:
...       print("Whoops! cannot divide")
...       return
...     return func(a,b)
...   return inner
...
>>> @smart_divide
... def divide(a,b):
...   return a/b

Такое представление вернёт None, если условие ошибки сработает:

>>> divide(2,5)
('I am going to divide', 2, 'and', 5)
0
>>> divide(2,0)
('I am going to divide', 2, 'and', 0)
Whoops! cannot divide

Таким образом мы можем декорировать функции, которые имеют параметры. Внимательный читатель должен был заметить, что параметры вложенной функции inner() внутри декоратора аналогичны параметрам функции, которая передаётся декоратору. Зная это — мы можем создать общий декоратор, который будет работать с любым количеством параметров. В Python это возможно с помощью function(*args, **kwargs). Таким образом — args будут являтся кортежем позиционных аргументов, а kwargs — словарём с ключевыми словами аргументов (именованные параметры).

Вот пример такого декоратора:

>>> def works_for_all(func):
...   def inner(*args, **kwargs):
...     print("I can decorate any function")
...     return func(*args, **kwargs)
...   return inner
...
>>> @works_for_all
... def foo(a, b, c):
...   print a, b, c
>>> foo('a', 'b', 'c')
I can decorate any function
a b c

Цепочки декораторов в Python

Несколько декораторов могут быть связаны в одну цепочку. Т.е. — одна функция может быть «декорирована» разными (или одним и тем же) декораторами. Мы просто располагаем декораторы перед желаемой функцией:

>>> def star(func):
...   def inner(*args, **kwargs):
...     print("*" * 30)
...     func(*args, **kwargs)
...     print("*" * 30)
...   return inner
...
>>> def percent(func):
...   def inner(*args, **kwargs):
...     print("%" * 30)
...     func(*args, **kwargs)
...     print("%" * 30)
...   return inner
...
>>> @star
... @percent
... def printer(msg):
...   print(msg)

В результате — мы получим такой вывод:

>>> printer("Hello")
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

Синтаксис:

@star
@percent
def printer(msg):
    print(msg)

Аналогичен использованию:

def printer(msg):
    print(msg)
printer = star(percent(printer))

Учтите, что порядок указания декораторов имеет значение. Если мы поменяем их местами:

>>> @percent
... @star
... def printer(msg):
...   print(msg)

То получим такой результат:

>>> printer("Hello")
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Оригинал статьи — тут>>>.