Одним из самых сложных для понимания и осознания элементов языка является декоратор, хотя по сути это очень простая вещь, доступная для понимания даже начинающему программисту. Новых Эверестов я не открываю, а лишь предлагаю краткий обзор возможностей и несколько типичных примеров использования. Этакий короткий экскурс в метапрограммирование на питоне.
Upd 1: изменил несколько категоричное утверждение о несходстве паттерна Декоратор и одноимённой языковой конструкции на более мягкое.
Итак, декоратор — это удобный способ изменения поведения некоторой функции (а начиная с Python 2.6 и 3.0 и целого класса). С точки зрения синтаксиса выглядит достаточно просто. Например, следующий фрагмент кода, использующий декоратор:
эквивалентен такому:
Слово «эквивалентен» нужно понимать буквально: операция выполняется в момент определения функции один раз и если f1 вернёт, скажем, None, то в переменной func будет записан None. Простой пример (декорирующая функция возвращает None, в итоге func тоже оказывается равным None):
Давайте рассмотрим более практичный пример. Допустим, нужно проверить, как быстро работает некоторая функция, нужно знать, сколько времени ��тнимает каждый её вызов. Задача элементарно решается при помощи декоратора.
Как видно из примера, чтобы заставить функцию func при каждом исполнении печатать время работы, достаточно «обернуть» её в декоратор timer. Закомментируем строчку «@timer» и func продолжает работать как обычно.
Функция timer() является самым типичным декоратором. В качестве единственного своего параметра она принимает функцию, внутри себя создаёт новую функцию (в нашем примере с именем tmp), в которой добавляет какую-либо логику и эту самую новую функцию возвращает. Обратите внимание на сигнатуру функции tmp() — tmp(*args, **kwargs), это стандартный способ «захватить» все возможные аргументы, таким образом, наш декоратор пригоден для функций с совершенно произвольной сигнатурой.
Функцию можно обернуть в несколько декораторов. В этом случае они «выполняются» сверху вниз. Например, создадим декоратор pause(), который будет делать паузу в одну секунду перед исполнением функции.
И определим функцию func следующим образом (используя сразу два декоратора — pause и timer):
Теперь вызов func(1, 2) покажет общее время исполнения примерно одну секунду.
Вам могло показаться, что в качестве декоратора можно использовать только функцию. Это не так. В качестве декоратора может выступать любой объект, который можно «вызвать». Например, в качестве декоратора может выступать класс. Вот значительно более сложный пример, показывающий, как можно конструировать потоки (threads) при помощи декораторов:
Давайте разберём подробно этот пример. «Классический» способ создания класса потока следующий: создаётся новый класс, наследник класса threading.Thread (threading — это стандартный модуль из Питона для работы с потоками); в классе задаётся метод run(), в который помещается непосредственно код, который нужно выполнить в отдельном потоке, затем создаётся экземпляр этого класса и для него вызывается метод start(). Вот как бы это выглядело в «классическом» варианте:
В нашем же случае декорируемая функция передаётся в качестве аргумента конструктору класса потока, где присваивается компоненту класса run.
Для создания нескольких разных потоков вам нужно дважды продублировать «классический» код. А при использовании «потоковых» декораторов — только добавить вызов декоратора к функции потока.
В декоратор можно передавать параметры, запись вида:
эквивалентна
По сути это означае, что декоратором является результат выполнения функции f1(123). Давайте напишем обновлённый декоратор pause(), который позволяет указывать величину паузы перед выполненением оборачиваемой функции:
Обратите внимание, как декоратор фактически создаётся динамически внутри функции pause().
Использование декораторов на методах классов ничем не отличается от использования декораторов на обычных функциях. Однако для классов есть предопределённые декораторы с именами staticmethod и classmethod. Они предназначены для задания статических методов и методов класса соответственно. Вот пример их использования:
Статический метод (обёрнутый декоратором staticmethod) в принципе соответствует статическим методам в C++ или Java. А вот метод класса — это нечто более интересное. Первым аргументом такой метод получает класс (не экземпляр!), это происходит примерно так же, как с обычными методами, которые первым аргументом получают референс на экземпляр класса. В случае, когда метод класса вызывается на инстансе, первым параметром передаётся актуальный класс инстанса, это видно на примере выше: для порождённого класса передаётся именно порождённый класс.
Список потенциальных областей применения декораторов очень большой:
Использовать декораторы нужно весьма осторожно, хорошо осознавая, чего именно вы хотите добиться. Излишнее их использование приводит к появлению слишком сложного для понимания кода. Можно в приступе озарения написать такое, что позже сам не разберёшься, как же написанное работает.
Использование декоратора ломает documentation strings для метода/функции. Проблему можно решить, вручную «пробрасывая» значение __doc__ в создаваемую внутри декоратора функцию. А можно воспользоваться замечательным модулем с неожиданным названием decorator, который помимо поддержки doc strings, умеет ещё множество других полезных вещей.
Upd 1: изменил несколько категоричное утверждение о несходстве паттерна Декоратор и одноимённой языковой конструкции на более мягкое.
В самом начале хочется отметить, что рассматриваемый здесь декоратор (decorator) как элемент языка Python не является реализацией одноимённого паттерна проектирования, его возможности гораздо шире, хотя сам паттерн и может быть реализован через питоновский декоратор.
Что такое декоратор и простейшие способы его использования
Итак, декоратор — это удобный способ изменения поведения некоторой функции (а начиная с Python 2.6 и 3.0 и целого класса). С точки зрения синтаксиса выглядит достаточно просто. Например, следующий фрагмент кода, использующий декоратор:
@f1 def func(x): pass
эквивалентен такому:
def func(x): pass func = f1(func)
Слово «эквивалентен» нужно понимать буквально: операция выполняется в момент определения функции один раз и если f1 вернёт, скажем, None, то в переменной func будет записан None. Простой пример (декорирующая функция возвращает None, в итоге func тоже оказывается равным None):
def empty(f):
return None
@empty
def func(x, y):
return x + y
print func # напечатает: None
Давайте рассмотрим более практичный пример. Допустим, нужно проверить, как быстро работает некоторая функция, нужно знать, сколько времени ��тнимает каждый её вызов. Задача элементарно решается при помощи декоратора.
import time
def timer(f):
def tmp(*args, **kwargs):
t = time.time()
res = f(*args, **kwargs)
print "Время выполнения функции: %f" % (time.time()-t)
return res
return tmp
@timer
def func(x, y):
return x + y
func(1, 2) # напечатает что-то типа: Время выполнения функции: 0.0004Как видно из примера, чтобы заставить функцию func при каждом исполнении печатать время работы, достаточно «обернуть» её в декоратор timer. Закомментируем строчку «@timer» и func продолжает работать как обычно.
Функция timer() является самым типичным декоратором. В качестве единственного своего параметра она принимает функцию, внутри себя создаёт новую функцию (в нашем примере с именем tmp), в которой добавляет какую-либо логику и эту самую новую функцию возвращает. Обратите внимание на сигнатуру функции tmp() — tmp(*args, **kwargs), это стандартный способ «захватить» все возможные аргументы, таким образом, наш декоратор пригоден для функций с совершенно произвольной сигнатурой.
Функцию можно обернуть в несколько декораторов. В этом случае они «выполняются» сверху вниз. Например, создадим декоратор pause(), который будет делать паузу в одну секунду перед исполнением функции.
import time
def pause(f):
def tmp(*args, **kwargs):
time.sleep(1)
return f(*args, **kwargs)
return tmp
И определим функцию func следующим образом (используя сразу два декоратора — pause и timer):
@timer
@pause
def func(x, y):
return x + yТеперь вызов func(1, 2) покажет общее время исполнения примерно одну секунду.
Более сложное использование декораторов
Вам могло показаться, что в качестве декоратора можно использовать только функцию. Это не так. В качестве декоратора может выступать любой объект, который можно «вызвать». Например, в качестве декоратора может выступать класс. Вот значительно более сложный пример, показывающий, как можно конструировать потоки (threads) при помощи декораторов:
import threading
class Thread(threading.Thread):
def __init__(self, f):
threading.Thread.__init__(self)
self.run = f
@Thread
def ttt():
print "This is a thread function"
ttt.start() Давайте разберём подробно этот пример. «Классический» способ создания класса потока следующий: создаётся новый класс, наследник класса threading.Thread (threading — это стандартный модуль из Питона для работы с потоками); в классе задаётся метод run(), в который помещается непосредственно код, который нужно выполнить в отдельном потоке, затем создаётся экземпляр этого класса и для него вызывается метод start(). Вот как бы это выглядело в «классическом» варианте:
class ThreadClassic(threading.Thread):
def run(self):
print "This is a thread function"
ttt = ThreadClassic()
ttt.start()В нашем же случае декорируемая функция передаётся в качестве аргумента конструктору класса потока, где присваивается компоненту класса run.
Для создания нескольких разных потоков вам нужно дважды продублировать «классический» код. А при использовании «потоковых» декораторов — только добавить вызов декоратора к функции потока.
Пример с потоком приведён исключительно в ознакомительных целях. В реальности его нужно использовать очень аккуратно, поскольку не весь потоковый код можно обернуть в описанный здесь декоратор.
В декоратор можно передавать параметры, запись вида:
@f1(123) def func(): pass
эквивалентна
def func(): pass func = f1(123)(func)
По сути это означае, что декоратором является результат выполнения функции f1(123). Давайте напишем обновлённый декоратор pause(), который позволяет указывать величину паузы перед выполненением оборачиваемой функции:
import time
def pause(t):
def wrapper(f):
def tmp(*args, **kwargs):
time.sleep(t)
return f(*args, **kwargs)
return tmp
return wrapper
@pause(4)
def func(x, y):
return x + y
print func(1, 2)Обратите внимание, как декоратор фактически создаётся динамически внутри функции pause().
Использование декораторов в классах
Использование декораторов на методах классов ничем не отличается от использования декораторов на обычных функциях. Однако для классов есть предопределённые декораторы с именами staticmethod и classmethod. Они предназначены для задания статических методов и методов класса соответственно. Вот пример их использования:
class TestClass(object):
@classmethod
def f1(cls):
print cls.__name__
@staticmethod
def f2():
pass
class TestClass2(TestClass):
pass
TestClass.f1() # печатает TestClass
TestClass2.f1() # печатает TestClass2
a = TestClass2()
a.f1() # печатает TestClass2
Статический метод (обёрнутый декоратором staticmethod) в принципе соответствует статическим методам в C++ или Java. А вот метод класса — это нечто более интересное. Первым аргументом такой метод получает класс (не экземпляр!), это происходит примерно так же, как с обычными методами, которые первым аргументом получают референс на экземпляр класса. В случае, когда метод класса вызывается на инстансе, первым параметром передаётся актуальный класс инстанса, это видно на примере выше: для порождённого класса передаётся именно порождённый класс.
Где ещё можно использовать декораторы
Список потенциальных областей применения декораторов очень большой:
- трассировка вызовов функции
- установка/проверка пре- и постусловий
- синхронизация
- ленивые вычисления и вообще кеширование вызовов функций
- убирание хвостовой рекурсии
- проверка типов аргументов функции
- и т.д.
Настоящие трудности
Использовать декораторы нужно весьма осторожно, хорошо осознавая, чего именно вы хотите добиться. Излишнее их использование приводит к появлению слишком сложного для понимания кода. Можно в приступе озарения написать такое, что позже сам не разберёшься, как же написанное работает.
Использование декоратора ломает documentation strings для метода/функции. Проблему можно решить, вручную «пробрасывая» значение __doc__ в создаваемую внутри декоратора функцию. А можно воспользоваться замечательным модулем с неожиданным названием decorator, который помимо поддержки doc strings, умеет ещё множество других полезных вещей.
Рекомендуемая литература
- http://www.phyast.pitt.edu/~micheles/python/documentation.html — страница документации модуля decorator
- www.ibm.com/developerworks/linux/library/l-cpdecor.html — статья про декораторы на IBM developerWorks (и перевод на русский)
- Mark Lutz: Learning Python, 3rd Edition, Chapter “Function Decorators”
- PEP 318: Function Decorators
- PEP 3129: Class Decorators (начиная с Python 2.6 и Python 3.0)
wiki.python.org/moin/PythonDecorators — статья из официальной Python wiki про декораторы