Очаровательный Python: Магия декораторов

Взгляд на новейшие возможности метапрограммирования в языке Python

Python сделал возможным метапрограммирование как таковое, но каждая новая версия добавляет свои новшества – и не всегда совместимые с предыдущими – в подходы к реализации метапрограммирования. Уже некоторое время существуют возможности работы с функциональным объектами первого класса – равно как и технологии использования магических атрибутов. В версии Python 2.2 был создан собственный механизм изготовления метаклассов, имеющий широкие возможности, но требовавший значительных интеллектуальных усилий со стороны пользователей. Теперь, в Python 2.4, возникли декораторы – новейший и на сегодня безусловно самый удобный путь к реализации большинства возможностей метапрограммирования.

Дэвид Мерц, автор, Gnosis Software, Inc.

Дэвид Мерц (David Mertz) - большой знаток в области открытых стандартов и только умеренно пугает многословием. С Дэвидом можно связаться по mertz@gnosis.cx его жизнь описывается более подробно на http://gnosis.cx/dW/. Предложения и комментарии по этой, предыдущей или будущей статьям приветствуются. Можете также посмотреть книгу Дэвида Text Processing in Python.



28.05.2007

Делать много, делая меньше

У декораторов есть общая черта с более ранними метапрограммными абстракциями, введенными в Python: они в действительности не делают ничего, что нельзя было бы сделать и без них. Как Микеле Симионато (Michele Simionato) и я указывали ранее в выпусках рубрики Очаровательный Python, даже в Python 1.5 можно было манипулировать созданием классов без обработчика "metaclass".

Все декораторы похожи своей предельной простотой. Все, что делает декоратор – модифицирует функцию или метод, которые определяются непосредственно после декоратора. Это было возможно всегда, но важнейшим стимулом для использования этой возможности стало появление встроенных функций classmethod() и staticmethod() в Python 2.2. В "старом стиле" можно было использовать вызов classmethod(), например, следующим образом:

Листинг 1. Типичный classmethod в "старом стиле"
class C:
    def foo(cls, y):
        print "classmethod", cls, y
    foo = classmethod(foo)

Хотя classmethod() является встроенным, в нем нет ничего необыкновенного; с тем же успехом можно было бы создать свою собственную функцию трансформации метода. Например:

Листинг 2. Типичный преобразователь метода в "старом стиле"
def enhanced(meth):
    def new(self, y):
        print "I am enhanced"
        return meth(self, y)
    return new
class C:
    def bar(self, x):
        print "some method says:", x
    bar = enhanced(bar)

Все, что делает декоратор – он позволяет вам избежать повторения имени метода, ставя декоратор рядом с первым упоминанием метода при его определении. Например:

Листинг 3. Типичный classmethod в "старом стиле"
class C:
    @classmethod
    def foo(cls, y):
        print "classmethod", cls, y
    @enhanced
    def bar(self, x):
        print "some method says:", x

Декораторы работают для обычных функций – точно так же, как и для методов в классах. Удивительно, насколько легко такая простая и, строго говоря, не обязательная перемена в синтаксисе улучшает работу и упрощает понимание программ. Декораторы можно объединять в цепочки, располагая их один за другим перед функцией определения метода. Здравый смысл подсказывает избегать нагромождения слишком большого количества декораторов, но использование нескольких подряд вполне уместно:

Листинг 4. Цепочка декораторов
@synchronized
@logging
def myfunc(arg1, arg2, ...):
    # ...do something
# decorators are equivalent to ending with:
#    myfunc = synchronized(logging(myfunc))
# Nested in that declaration order

Будучи просто синтаксической изюминкой, декораторы позволяют легко делать глупости, если вам этого хочется. Декоратор – это просто функция, принимающая как минимум один аргумент. Это программист, использующий декоратор, должен удостовериться, что возвращаемая величина является все еще осмысленной функцией или методом и сохраняет достаточную часть их первоначальных свойств, чтобы результат его действия был полезным. Вот пара примеров неправильного использования синтаксиса:

Листинг 5. Плохой декоратор, который даже не возвращает функцию
>>> def spamdef(fn):
...     print "spam, spam, spam"
...
>>> @spamdef
... def useful(a, b):
...     print a**2 + b**2
...
spam, spam, spam
>>> useful(3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: 'NoneType' object is not callable

Другой вариант: декоратор может возвращать функцию, но не имеющую осмысленной связи с недекорированной функцией:

Листинг 6. Декоратор, чья выходная функция игнорирует входную функцию
>>> def spamrun(fn):
...     def sayspam(*args):
...         print "spam, spam, spam"
...     return sayspam

...
>>> @spamrun
... def useful(a, b):
...     print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam

Наконец, более порядочный декоратор должен некоторым образом улучшать или изменять действие недекорированной функции:

Листинг 7. Декоратор, который изменяет поведение недекорированной функции
>>> def addspam(fn):
...     def new(*args):
...         print "spam, spam, spam"
...         return fn(*args)
...     return new
...
>>> @addspam
... def useful(a, b):
...     print a**2 + b**2
...
>>> useful(3,4)
spam, spam, spam
25

Можно усомниться, действительно ли так полезна функция useful() и является ли addspam() таким уж замечательным улучшением, но, по крайней мере, механизмы те же, что обычно применяются в полезных декораторах.


Введение в абстракции высокого уровня

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

Листинг 8. Реально работающий, хотя и глубоко вложенный, декоратор
def arg_sayer(what):

    def what_sayer(meth):
        def new(self, *args, **kws):
            print what
            return meth(self, *args, **kws)
        return new
    return what_sayer

def FooMaker(word):
    class Foo(object):
        @arg_sayer(word)
        def say(self): pass
    return Foo()

foo1 = FooMaker('this')
foo2 = FooMaker('that')
print type(foo1),; foo1.say()  # prints: <class '__main__.Foo'> this
print type(foo2),; foo2.say()  # prints: <class '__main__.Foo'> that

В примере с @arg_sayer() весьма значительными усилиями достигается весьма скромный результат, но на нем стоит остановиться, чтобы понять несколько вещей, которые он иллюстрирует:

  • Метод Foo.say() ведет себя по-разному для разных экземпляров. В примере все различие сводится к значению величины, которое можно было бы изменять и другим способом, но, вообще говоря, декоратор мог бы переписывать метод полностью, основываясь на информации, получаемой в момент исполнения.
  • Недекорированный метод Foo.say() в этом случае представляет собой простую "заглушку", а все поведение определяется декоратором. Однако в других случаях декоратор может объединять поведение недекорированного метода с некоторыми новыми способностями.
  • Как уже говорилось, модификация Foo.say() определяется строго во время исполнения, путем использования фабрики классов FooMaker(). Возможно, более типичным является применение декораторов к классам, определенным на верхнем уровне, которые зависят только от условий, доступных во время компиляции (зачастую этого достаточно).
  • Декоратор параметризирован. Или, скорее, arg_sayer() сам по себе вообще не является реально декоратором. Скорее уж функция, возвращаемаяarg_sayer(), а именно what_sayer(), представляет собой функцию декоратора, которая использует оболочку для инкапсулирования данных. Параметризация декораторов является обычным делом, но она приводит к необходимости вложения необходимых функций на трехуровневую глубину.

Поход к метаклассам

Как отмечено в предыдущем разделе, декораторы не позволили полностью заменить обработчик metaclass, так как они только модифицируют, а не добавляют или удаляют методы. В действительности это не совсем правда. Декоратор, будучи функцией Python’а, может делать абсолютно все, что может делать остальной Python’овский код. Декорируя метод .__new__() класса, даже его "заглушку", вы на самом деле можете изменять выбор методов, которые присоединяются к классу. Я не сталкивался с таким подходом в реальной жизни, но я думаю, что в нем есть определенная прозрачность, и его даже можно рассматривать как усовершенствование _metaclass_:

Листинг 9. Декоратор для добавления и удаления методов
def flaz(self): return 'flaz'     # Silly utility method
def flam(self): return 'flam'     # Another silly method

def change_methods(new):
    "Warning: Only decorate the __new__() method with this decorator"

    if new.__name__ != '__new__':
        return new  # Return an unchanged method
    def __new__(cls, *args, **kws):
        cls.flaz = flaz
        cls.flam = flam
        if hasattr(cls, 'say'): del cls.say
        return super(cls.__class__, cls).__new__(cls, *args, **kws)
    return __new__

class Foo(object):
    @change_methods
    def __new__(): pass
    def say(self): print "Hi me:", self

foo = Foo()
print foo.flaz()  # prints: flaz
foo.say()         # AttributeError: 'Foo' object has no attribute 'say'

В примере декоратора change_methods(), в сущности, бесцельно добавляются и удаляются некоторые фиксированные методы. В более реалистичном случае использовались бы некоторые образцы из предыдущего раздела. Например, параметризованный декоратор мог бы принимать структуру данных, указывающую методы, подлежащие добавлению или удалению. Или, возможно, решение могло бы приниматься на основании некоторого свойства среды, например, запроса к базе данных. Такие манипуляции с присоединенными методами можно по-прежнему упаковать в фабрику функций, отложив окончательное решение до времени исполнения. Эти последние подходы могут быть даже более универсальными, чем применение _metaclass_. Например, вы можете вызвать улучшенный change_methods() следующим образом:

Листинг 10. Улучшенный change_methods()
class Foo(object):
    @change_methods(add=(foo, bar, baz), remove=(fliz, flam))
    def __new__(): pass

Изменение модели вызова

Вероятнее всего, самое типичное применение декораторов, с которым вам придется сталкиваться – это оснащение функции или метода какими-либо дополнительными функциями вдобавок к их основному назначению. Например, на Web-сайте Python Cookbook (см. ссылку в разделе Ресурсы) и в других подобных местах можно встретить декораторы, добавляющие такие возможности, как трассировка, ведение журнала, запоминание/кэширование, блокировка потоков, перенаправление вывода. Сходны с такими модификациями, хотя и немного в другом духе, модификации "до" и "после". Одно интересное применение "до/после"-декорирования – это проверка типов аргументов и возвращаемого значения функции. Предполагается, что подобный декоратор type_check() должен вызвать исключение или предпринять какие-нибудь корректирующие действия, если типы не те, которые ожидаются.

Продолжая мыслить в духе "до/после"-декораторов, я дошел до идеи "поэлементного" применения функций, типичного для языка программирования R и NumPy. В этих языках численные функции обычно действуют на каждый элемент в последовательности элементов, а также и на отдельное число.

Разумеется, функция map(), построители списков (list-comprehensions) и с недавнего времени построители генераторов (generator-comprehensions) позволяют осуществлять поэлементное применение. Но для этого приходится прибегать к мелким ухищрениям, чтобы сымитировать возможности языка R: последовательность, возвращаемая map(), всегда имеет тип «список», и вызов завершится ошибкой, если вы передадите ей отдельный элемент, а не последовательность. Например:

Листинг 11. Вызов map(), который завершится ошибкой
>>> from math import sqrt
>>> map(sqrt, (4, 16, 25))
[2.0, 4.0, 5.0]
>>> map(sqrt, 144)
TypeError: argument 2 to map() must support iteration

Нетрудно создать декоратор, который "улучшает" обычную численную функцию:

Листинг 12. Преобразование функции в поэлементную функцию
def elementwise(fn):
    def newfn(arg):
        if hasattr(arg,'__getitem__'):  # is a Sequence
            return type(arg)(map(fn, arg))
        else:
            return fn(arg)
    return newfn

@elementwise
def compute(x):
    return x**3 - 1

print compute(5)        # prints: 124
print compute([1,2,3])  # prints: [0, 7, 26]
print compute((1,2,3))  # prints: (0, 7, 26)

Нетрудно, конечно, просто написать функцию compute(), в которую будут встроены разные типы возвращаемых значений; в конце концов, декоратор занимает всего несколько строк. Однако в качестве своего рода "реверанса" в сторону аспектно-ориентированного программирования этот пример позволяет нам разделить задачи разных уровней. Мы могли бы написать множество функций для численных вычислений и захотеть перевести каждую из них на поэлементную модель вызова, не задумываясь о тестировании типов аргументов и принудительной установки типа возвращаемого значения.

Декоратор elementwise() работает одинаково хорошо для любой функции, оперирует ли она отдельным элементом или последовательностью элементов (сохраняя тип последовательности). В качестве упражнения можете попробовать придумать, как сделать, чтобы тот же декорированный вызов мог также принимать и возвращать итераторы (подсказка: легко, если вы просто итерируете полностью поэлементное вычисление, менее очевидно, если на вход подается объект итератора).

Большинство хороших декораторов, с которыми вам придется сталкиваться, в значительной степени использует эту парадигму объединения ортогональных задач (concerns). В традиционном объектно-ориентированном программировании, особенно на таких языках как Python, которые допускают многократное наследование, предпринимается попытка модуляризировать задачи в рамках иерархии наследования. Однако для того, чтобы просто брать одни методы от одного родителя, а другие методы - от других родителей, необходима концепция, в которой задачи разделены намного сильнее, чем это имеет место в аспектно-ориентированном понимании. Чтобы максимально эффективно использовать генераторы, необходимо смотреть на проблемы несколько иначе, чем при простом комбинировании методов: любой метод можно сделать работающим по-разному в зависимости от задач, которые находятся вне "сердцевины" самого метода.


Декорирование декораторов

Прежде чем я закончу этот выпуск, хочу обратить ваше внимание на действительно замечательный модуль Python’а под названием decorator, написанный Микеле Симионато (Michele Simionato), с которым мы иногда соавторствуем. Этот модуль делает разработку декораторов намного приятнее. Не без некоторой рефлексивной элегантности, основная компонента модуля decorator – декоратор под названием decorator(). Функция, декорированная посредством @decorator, может быть написана в более простой манере, чем без него (см. Ресурсы).

Микеле разработал очень хорошую документацию для своего модуля, и я не буду пытаться воспроизводить ее здесь; однако я хотел бы указать на основные проблемы, которые он решает. Два главных преимущества использования модуля decorator: с одной стороны, он позволяет писать декораторы с меньшей степенью вложенности, чем позволяют другие средства ("плоский лучше, чем вложенный"). Возможно, еще интереснее то, что декорированная функция фактически соответствует своей недекорированной версии в метаданных, чего нет в моих примерах. Например, вспомним "глуповатый" "трассирующий" декоратор addspam(), который я использовал выше:

Листинг 13. Как "глупый" декоратор искажает метаданные
>>> def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> from inspect import getargspec
>>> getargspec(useful)
(['a', 'b'], None, None, None)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'new'
>>> getargspec(useful)
([], 'args', None, None)

При более пристальном рассмотрении становится заметно, что хотя декорированная функция исполняет свою "улучшенную" работу, она делает это не совсем правильно, особенно с точки зрения инструментов анализа кода или интегрированных сред разработки, которые заботятся о подробностях такого рода. Использование decorator позволяет улучшить ситуацию:

Листинг 14. Более изящное использование декоратора
>>> from decorator import decorator
>>> @decorator
... def addspam(f, *args, **kws):
...     print "spam, spam, spam"
...     return f(*args, **kws)
>>> @addspam
... def useful(a, b): return a**2 + b**2
>>> useful.__name__
'useful'
>>> getargspec(useful)
(['a', 'b'], None, None, None)

Такой вариант выглядит лучше и с точки зрения написания декоратора, и в том, что он при этом сохраняет метаданные. Конечно, для понимания всех заклинаний, которые Микеле использовал при разработке своего модуля, вам пришлось бы вернуться обратно в мозгоплавильную область; мы можем спокойно оставить это для космологов, подобных д-ру Симионато.

Ресурсы

Научиться

  • Charming Python: Decorators make magic easy Оригинал данной статьи на developerWorks.
  • ASPN online Python Cookbook -- страница ActiveState Programmer Network, где можно найти множество полезных примеров использования декораторов, а также другие изощренные примеры на Python.
  • Микеле Симионато (Michele Simionato) в своей онлайновой документации к Python рассматривает модуль decorator для Python 2.4, а также многочисленные изменения, касающиеся Python 2.5.
  • Дополнительно ознакомьтесь с серией из двух статей Дэвида и Микеле на developerWorks "Metaclass programming in Python" (Программирование метаклассов в Python).
  • Полезно ознакомиться с введением в "Statistical programming with the R programming language" (Статистическое программирование на языке R) в серии из трех статей Дэвида и Брэда Хантинга (Brad Huntting) на developerWork.
  • Читайте "Charming Python: Numerical Python" (developerWorks, Октябрь 2003) про NumPy и, вскользь, про приложения для "поэлементных (elementwise)" функций.
  • Хорошей отправной точкой для только знакомящихся с этой тематикой будет статья в Википедии об аспектно-ориентированном программировании.
  • В разделе developerWorks по Linux найдется много полезных ресурсов для разработчиков Linux.
  • Регулярно посещайте раздел технических мероприятий и Web-трансляций developerWorks, чтобы всегда быть в курсе событий и новостей.

Получить продукты и технологии

  • Вы можете непосредственно скачать пробные версии программ с сайта ознакомительных версий ПО IBM и использовать их в своих собственных проектах для Linux.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=226733
ArticleTitle=Очаровательный Python: Магия декораторов
publish-date=05282007