6 декабря 2008 в 06:24

Сила и красота декораторов

Одним из самых сложных для понимания и осознания элементов языка является декоратор, хотя по сути это очень простая вещь, доступная для понимания даже начинающему программисту. Новых Эверестов я не открываю, а лишь предлагаю краткий обзор возможностей и несколько типичных примеров использования. Этакий короткий экскурс в метапрограммирование на питоне.

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, умеет ещё множество других полезных вещей.

Рекомендуемая литература

  1. http://www.phyast.pitt.edu/~micheles/python/documentation.html — страница документации модуля decorator
  2. www.ibm.com/developerworks/linux/library/l-cpdecor.html — статья про декораторы на IBM developerWorks (и перевод на русский)
  3. Mark Lutz: Learning Python, 3rd Edition, Chapter “Function Decorators”
  4. PEP 318: Function Decorators
  5. PEP 3129: Class Decorators (начиная с Python 2.6 и Python 3.0)
    wiki.python.org/moin/PythonDecorators — статья из официальной Python wiki про декораторы
+96
18294
194
Cancel 28,2

Комментарии (30)

0
cleg, #
теперь можете перенести в блог Python :-)
0
Cancel, #
Спасибо, перенёс :)
0
eschava, #
почему это не имеет ничего общего с паттерном? похоже, что это он и есть. в современной литературе его еще называют AOP-подходом

и почему в статье нет примеров или даже перечисления реального использования декораторов? книжные примеры «посчитать время выполнения функции» совсем не дают понятия, как их можно использовать в реальной жизни
+1
cleg, #
к сожалению, декораторы — это весьма условная реализация АОР. очень условная, к тому же смешивающая Advices и PoI.
а на счет примеров — так приведите несколько хороших, все были бы благодарны
+2
krig, #
Из примеров реального использования декораторов можно было бы привести примр декоратора @login_required из Django.
+2
eschava, #
примеры (первое, что приходит в голову) —
— проверка прав текущего пользователя для выполнения функции
— прозрачная работа с транзакциями БД (commit, rollback)
— кеширование результатов выполнения функции
0
Cancel, #
>— прозрачная работа с транзакциями БД (commit, rollback)

Для этого есть with и контексты, они гораздо лучше справляются с этой работой. Но это уже придирки с моей стороны. Подобных примеров можно найти десятки, они ничуть не лучше уже приведённых. Хотя, конечно, можно привести в качестве иллюстрации. Но повторюсь, они не добавляют бо́льшего понимания, относительно уже приведённых примеров.
0
Cancel, #
Сходство весьма поверхностное. С таким же успехом можно было сравнивать с простой суперпозицией функций во время вызова. Адекватная реализация этого паттерна в питоне смотрится как-то чужеродно, впрочем, как и реализация практически любого объектно-классового паттерна. Но это уже тема отдельной дискуссии и огромного флейма на тему ООП vs. ФП.

«Посчитать время выполнения функции» — это отличный пример, отлично поясняющий суть декораторов. Аналогично с классом Thread. Другие примеры, например, с PyQt и декорированными обработчиками сигналов, используют вещи, далеко не всем знакомые и требующими кучи вводного текста.

Декораторы действительно сильно напоминают AOP, но проблема в том, что связывание декоратора с функцией происходит в момент прохода интерпретатором непосредственно определения функции. Это как минимум одна проблема. Для AOP мне показалось проще использовать тот же aspect.py, чем заморачиваться с декораторами.
0
Exabiche, #
Спасибо, познавательно.
–1
mordet, #
Присоединяюсь к благодарности, загорелся желанием познакомиться с питоном. ;-)
+1
google_fan, #
Добавьте еще про декораторы классов <a href=«www.python.org/dev/peps/pep-3129/»">PEP-3129
0
Cancel, #
Да, спасибо. Добавил ссылку. Код пока не могу добавить, так как живьём ещё «не щупал».
0
glader, #
Интересно, единственное применение, котрое мне приходит в голову — это добавление доп. методов. А это проще делать наследованием. Где полезно применять декораторы классов?
+1
Cancel, #
Наследование не везде подходит, у тех же GOF в вводных главах («Наследование и композиция») этот вопрос рассматривается. Правда, там есть неявные завязки на природу C++ и прочих подобных языков. К сожалению, не могу скопипастить, так как под рукой только бумажная книга.
–1
stoune, #
Фразы «В самом начале хочется отметить, что рассматриваемый здесь декоратор (decorator) как элемент языка Python не имеет никакого отношения к одноимённому паттерну проектирования.» хватает чтобы не читать дальше. Или вы в самом паттерне не разобрались или в его реализации на Питоне.

Почитайте Еккеля
www.artima.com/weblogs/viewpost.jsp?thread=240808
www.artima.com/weblogs/viewpost.jsp?thread=240845
www.artima.com/weblogs/viewpost.jsp?thread=241209

www.mindviewinc.com/Books/Python3Patterns/Index.php
он всё таки более основательно к написанию статтей подходит.
0
stoune, #
Открываю GOF.
«Назначение: Динамически добавляет объекту новые обязаности. Является гибкой альтернативой порожению подкласов с целью расширения функциональности». Что не так с питоновским декоратором и чем он не отвечает определению?
+3
Cancel, #
Еккель — джавист, его т.н. «книга» Python 3 Patterns является попыткой навязать джавовский классовый подход к питону, а это неправильно. Посмотрим, во что она в итоге выльется, но пока отношение к ней у меня сугубо негативное.

Теперь к теме вопроса. Паттерн Декоратор имеет весьма условное отношение к одноимённой концепции из питона, а именно — питоновский декоратор может использоваться для реализации паттерна, но это очень небольшая часть, для чего он может использоваться. С таким подходом банальную суперпозицию фукций можно тоже объявить реализацией паттерна. Схожесть концепций не более чем схожесть. Наверное, процитированное утверждение может показаться слишком категоричным, я его исправлю.
0
stoune, #
Прочитайте пожалуйста wiki.python.org/moin/PythonDecorators. И На питоновской вики чётко указывается: «This supports more readable applications of the DecoratorPattern but also other uses as well.» то есть в ПЕРВУЮ очередь синтаксический сахар для реализации паттерна, но можно использовать и для других вещей. Конкретная реализация более мощная, но уже само название указывает первоначальное предназначение.

Именно ваша категоричность указывает либо непонимание либо нежелание принять действительность и навязать собственное видение. Не нравится АОП, но декораторы тут причём? АОП начали продвигать где-то в 2004-м. GOF вышла в 1995-м.
Новичёк открывает раздел на хабрахабре Питоновкую вики и видит такое утверждение и потом всюду будет приводить его как истину, тогда либо пишите статью для професионалов с изложением теоретических и практических концепций декорирования и вашей обоснованой или нет критикой, а вводные статьи таких утвеждений иметь не должны, потому как запутывают.
+1
Cancel, #
Да, я понял вашу мысль, впредь постараюсь быть поаккуратнее в формулировках.
0
Cancel, #
Таки прочитал топик: «Indeed, you can use Python decorators to implement the Decorator pattern, but that's an extremely limited use of it. Python decorators, I think, are best equated to macros.» Примерно это я и имел в виду, практически дословно получилось :)
+3
bsdemon, #
Для того чтобы не ломалось ни __doc__ ни имя функции используйте functools.wraps
+1
mrShadow, #
Мне больше понравилась глава о декораторах в черновике новой книги Брюса Эккеля Python 3 Patterns & Idioms. О книге я узнал из этой темы.
0
mrShadow, #
По-моему, в примере с классом Thread лучше реализовать декоратор так, чтобы функцию можно было вызывать традиционным способом, а не используя метод start(). Вроде так:
import threading

class Thread(threading.Thread):
    def __init__(self, f):
        threading.Thread.__init__(self)
        self.run = f
    def __call__(self):
        return self.start()

@Thread
def ttt():
    print "This is a thread function"

ttt()
0
supersasha, #
Книжка пока оочень сыровата.
0
mrShadow, #
Но глава о декораторах уже написана :)
0
Cancel, #
И уже откритикована, в том числе и ван Россумом. Эккель навязывает «классовый» подход, но явно об этом факте не упоминает, возможно, в финальной редакции книги это будет указано.
0
supersasha, #
Да, я читал, но мне что-то не понравилось. Он и сам признается, что Питон пока не очень хорошо знает. По Яве книжку замечательно написал, слов нет. Будем ждать то же самое по Питону.
0
karguine, #
Вот это интересно. Человек, написавший книгу по питону, признаётся, что не очень хорошо знает питон.
0
supersasha, #
Не написавший, а пишущий. В этом нет ничего удивительного. Так же зачастую мы начинаем писать программу на языке, которого не знаем. К тому же Эккель, как человек, написавший «Философию Java» вполне достоин доверия.
+1
supersasha, #
Хорошая статья, только я бы избегал использования жаргона:
референс = ссылка
инстанс = экземпляр
актуальный = действительный

Только зарегистрированные пользователи могут оставлять комментарии.
Войдите, пожалуйста.