Pull to refresh

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

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

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 про декораторы
Tags:
Hubs:
+96
Comments30

Articles

Change theme settings