Пользователь
0,0
рейтинг
26 апреля 2013 в 00:00

Разработка → Добавляем чуть больше рефлексии: декораторы

Последнее время приходится довольно много работать с Python. Решая одну из текущих задач, возникла необходимость внутри функции-декоратора проверить задекорирован ли декорируемый метод другим декоратором. К сожалению, стандартные средства рефлексии языка не позволяют это сделать. Точнее, используя, например, модуль inspect из стандартной библиотеки это сделать можно, но уж больно не нравился такой подход.

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

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

Тем не менее есть несколько правил, о которых слдедует помнить. Регистрация built-in декораторов не будет работать. То есть, к сожалению, не получится трэкать такие декораторы, как @staticmethod, @classmethod и им подобные (если кто-то сможет найти решение этой проблемы — буду премного благодарен). И самое главное, — декораторы должны быть зарегистрированы до их использования.

Фактически, механика работы реестра достаточно проста. Регистрируя декоратор вы фактически получаете задекорированый исходный декоратор который помимо своей исходной функциональности также записывает информацию о себе в аттрубут "__annotations__" декорируемой функции.

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

@decorator_one
@decorator_two
@decorator_three
def some_function():
    pass


будет успешно работать.

Библиотека «regd» (так я ее назвал), совместима как с Python 2.x, так и с версией 3.x (на наших проектах у нас используются обе ветки, поэтому совместимость проверялась).

Исходники доступны на Github, лицензия, как всегда — MIT, так что делайте все, что захотите.

Документация здесь.

Установить можно просто через PyPI:

$ pip install regd


Ниже несколько слов о функционале.

1. Регистрация декораторов.

«Обычные» и «параметризованые» декораторы должны регистрироваться разными методами:

from regd import DecoratorRegistry as dreg

# регистрация "обычного" декоратора
simple_decorator = dreg.decorator( mydecorator)

# регистрация "параметризованного" декоратора
param_decorator = dreg.parametrized_decorator( param_decorator)


2. API рефлексии

Чтобы функции API были более понятными давайте для начала создадим и зарегистрируем простой декоратор, который по факту ничнего не делает, а просто существует.

from regd import DecoratorRegistry as dreg

# создадим декоратор
def mydecorator( fn) :
    # здесь может быть какая-то полезная работа...
    def wrapper( *args, **kwargs)
        # ... или здесь что-то полезное ...
        return fn( *args, **kwargs)
    return wrapper

# зарегистрируем наш декоратор
mydecorator = dreg.decorator( mydecorator)


Помните — зарегистрировать декораторы нужно до их использования.

Теперь, после регистрации, можем использовать наш декоратор как обычно:

@mydecorator
def myfunc() :
    pass


Теперь в любой момент из любого места в коде можем узнать, задекорирована ли функция декоратором:
print( dreg.is_decorated_with( myfunc, mydecorator))


Еще несколько полезных методов:

  • all_decorated_module_functions( module, exclude_methods=False, exclude_functions=False) — позволяет получить все функции и/или методы классов задекорированные зарегистрированными декораторами в заданном модуле
  • module_functions_decorated_with( module, decorator, exclude_methods=False, exclude_functions=False) — позволяет получить все функции и/или методы классов задекорированные заданным декоратором в заданном модуле
  • decorated_methods( cls, decorator) — получаем все методы класса/объекта задекорированные заданным декоратором
  • get_decorators( fn) — вернет список все известных декораторов для заданной функции/метода
  • get_real_function( fn) — вернет ссылку ра исходную функцию, которая была задекорирована декораторами (да, можно получить доступ к исходной функции и даже выполнить ее в обход декорирования)
  • is_decorated_with( fn, decorator) — проверяет, задекорирована ли заданная функция заданным декоратором


Надеюсь, кому-то пригодится или покажется полезным. Все замечания и предложения приветствуются.
Михаил Стадник @Mikhus
карма
115,7
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +1
    Интересно,
    А если вместо
    def mydecorator(fn) :
        # здесь может быть какая-то полезная работа...
        def wrapper(*args, **kwargs)
            # ... или здесь что-то полезное ...
            return fn(*args, **kwargs)
        return wrapper
    
    # зарегистрируем наш декоратор
    mydecorator = dreg.decorator(mydecorator)
    


    написать
    @dreg.decorator
    def mydecorator( fn) :
        # здесь может быть какая-то полезная работа...
        def wrapper( *args, **kwargs)
            # ... или здесь что-то полезное ...
            return fn( *args, **kwargs)
        return wrapper
    


    — не выйдет ли проще для понимания?
    • –2
      так не сработает
      • +3
        Как это может не сработать, если это и есть определение декоратора?
        • 0
          Виноват, засыпаю. конечно же будет работать и так
  • +5
    > возникла необходимость внутри функции-декоратора проверить задекорирован
    > ли декорируемый метод другим декоратором
    Зачем?
    • 0
      Вот я ожидал этого вопроса и даже начал описывать кейс, но потом передумал. Вы готовы поверить на слово, что на то были объективные причины?
      • +5
        Нет. Скорее всего вы нашли не самое хорошее решение.
        • 0
          Может быть оно и не самое хорошее но наиболее удобное, так как позволяет не править сторонюю библиотеку и иметь впоследствии проблемы с ее обновлением. К сожалению описание деталей тянет на отдельную статью… Ваше право считать так как вы считаете.
          • 0
            > наиболее удобное, так как позволяет не править сторонюю библиотеку
            Вы заранее считаете, что любое другое решение потребует правки. Возможно, это не так.
            • 0
              Я считаю не заранее, а по факту.
      • +2
        Это все очень странно выглядит и больше похоже на статью из топика «Ненормальное программирование»… может всё-таки набросаете жизненный пример для общего понимания?
        • +1
          Вот жеж вы зануды.
          Хорошо, пусть будет такой пример. Начнем с того, что есть сторонняя библиотека «один» поставляющая некий «мега-функционал», который «включается» декоратором «one». Есть вторая, такая же важначя и нужная и поставляет декоратор «two».

          Есть некий наш класс:

          class MyClass(object):
          
              def my_method(self):
                  pass
          


          Если вы применяете декораторы из двух библиотек по одному на ваших методах — все работает. Так работает:

          class MyClass(object):
          
              @one
              def my_method(self):
                  pass
          


          и так работает:

          class MyClass(object):
          
              @two
              def my_method(self):
                  pass
          


          А так не работает:

          class MyClass(object):
          
              @one
              @two
              def my_method(self):
                  pass
          


          и так не работает:

          class MyClass(object):
          
              @two
              @one
              def my_method(self):
                  pass
          


          Не работает потому, что сами библиотеки внутри возможно написаны не очень хорошо (такое бывает?) и каждая из них делает нечно, для чего ожидает что переданный в ее декоратор метод — это метод класса, патается каким-то образом отрефлектить этот метод, и что-то там сделать — не важно. Поэтому в таком варианте не работает по той причине что верхний декоратор декорирует уже задекорированый метод, а не исходный. В качестве решения я и воспользовался таким подходом — регистрирую оба декоратора в реестре и один из них переопределяю, передавая реальную функцию, когда она не доступна. Остальной функционал по рефлексии получился в качестве дополнительного инструментария, что иногда полезно, как минимум в дебаге. Сторонние же библиотеки остались нетронутыми, и не будет проблем с их обновлением. Может это и не очень правильно, как вы говорите, но зато удобно.
          • 0
            > сами библиотеки внутри возможно написаны не очень хорошо (такое бывает?)
            И проще написать еще одну, лишь бы не исправлять эти? :)

            > не будет проблем с их обновлением
            Просто исправьте их и судя по вашему рассказу не будет вообще никаких проблем ни с чем.
            • 0
              > И проще написать еще одну, лишь бы не исправлять эти? :)
              Ну вам уже не придется так напрягаться — можете воспользоваться моей :)

              > Просто исправьте их и судя по вашему рассказу не будет вообще никаких проблем ни с чем.
              Да и так нет проблем ни с чем
            • 0
              Вообще-то, если по-честному, я немного лукавлю, говоря, что библиотеки написаны не очень хорошо. Просто сам Python, на мой взгляд устроен не совсем хорошо. Сам факт того, что при декорировании, метод переданый в декоратор становиться unbound функцией и зарефлектить, к какому классу принадлежит данный метод, не представляется возможным без таких вещей, как f.__code__.co_filename, f.__code__.co_firstlineno, и с последующим парсингом исходника с целью найти какому классу принадлежит данный метод. Если же декораторов было несколько, то вы вообще теряете связь с самим исходным методом! Вам опять может стать непонятным для чего это нужно, но ведь, в целом, рефлексии позволяют создавать некоторые абстракции, что может быть полезным при проектировании некоторых библиотек общего назначения. Все было бы гораздо проще, если бы на уровне языка рефлексии, да и реализация декораторов, были бы немного продуманнее.
              • 0
                Если честно, вы говорите на непонятном языке :) Нет никаких рефлексий, вся информация доступная интерпретатору доступна в рантайме и вам.

                > при декорировании, метод переданый в декоратор становиться unbound функцией
                Он и до декорирования обычная функция. Он привязывается к классу строго в тот момент, когда вы получаете его как атрибут, не раньше. Это и позволяет вам сделать декоратор для метода: все что вы вернете из декоратора тоже станет «методом». Взгляните на пример:

                def method(self):
                  return id(self)
                
                class A(object):
                  id = method
                
                class B(object):
                  id = method
                
                A().id()
                B().id()
                

                Какому классу «принадлежит» метод method по вашему мнению?

                Я думаю вам нужно больше узнать про дескрипторы, благодаря которым все это и работает. А так вы со своим уставом ищете правды. И пожалуйста, не пишите библиотеку для парсинга исходников :)
                • –2
                  Дорогой КО, зачем вы мне это пишете?
                  Я говорю именно о том, что подобный подход мне не нравится в Python.

                  Ваш пример:

                  >>> def m():
                  ...     pass
                  ... 
                  >>> def m(self):
                  ...     pass
                  ... 
                  >>> class A(object):
                  ...     f = m
                  ... 
                  >>> class B(object):
                  ...     f = m
                  ... 
                  >>> A.f.im_class
                  <class '__main__.A'>
                  >>> B.f.im_class
                  <class '__main__.B'>
                  >>> m.im_class
                  Traceback (most recent call last):
                    File "<stdin>", line 1, in <module>
                  AttributeError: 'function' object has no attribute 'im_class'
                  


                  Теперь объявляем класс с таким задекорированым методом:

                  >>> def d( fn):
                  ...     print fn.im_class
                  ...     def w(*a, **k):
                  ...             return fn(*a, **k)
                  ...     return w
                  ... 
                  >>> class X(object):
                  ...     @d
                  ...     def f(self):
                  ...             pass
                  ... 
                  Traceback (most recent call last):
                    File "<stdin>", line 1, in <module>
                    File "<stdin>", line 2, in X
                    File "<stdin>", line 2, in d
                  AttributeError: 'function' object has no attribute 'im_class'
                  


                  Все вы даже определить класс не можете! Чтобы это все таки работало — будете парсить код. Я не вижу другого решения. Вы видите?

                  И еще раз — я не пишу библиотеку «для парсинга исходников». Я просто использую библиотеку, у которой это внутри.
                  • 0
                    Э… а functools.wraps, случайно, не спасёт отца русской демократии ©? ;)
                    С функциями — спасает, с методами сам не пробовал…
                    • 0
                      Э… как бы… совсем о другом будет, не?
                      • +1
                        Не знаю — честно спросил :).

                        Попробовал. Не спасает.
                        И декоратор-класс тоже не спасает.

                        В декоратор уже попадает функция…
                  • 0
                    Продолжу…

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

                    Кстати, если кто-то может ткнуть меня мордой в то, как обойти парсинг исходников в такой ситуации — я буду просто благодарен и наконец-то стану спать спокойно.

                    Усложняем модель.

                    Вводим второй декоратор, который делает в какой-то мере то же самое (опять парсит, опять единожды при запуске, но делает что-то уже другое, но нам также полезное). Но так как второй декоратор будет декорировать не метод класса (а по написанному коду — это метод, объявленный внутри класса — так код написан!), а декоратор этого метода, то выходит, что будем парсить далеко не тот кусок кода, который ожидаем. (напоминаю — это не мое, чужое, но мне с ним жить)

                    Вот тут и поможет моя библиотека. Она позволяет получить get_real_function(). Но это будет доступно только если оба декоратора «зарегистрированы» перед определением класса. При этом декоратор, который используется сверху, переопределяем, подсовывая нативному реальную функцию. И все работает. И овцы целы, и волки сыты. Кстати можно переопределить таким способом оба и тогда можно вообще забить на порядок их следования при написании кода классов.

                    А теперь уж, я надеюсь, не составляет труда представить ситуацию, когда может понадобится проверить, а не задекорирован ли какой-то метод каким-то декоратором. А раз может, то и мы можем такую возможность в нашей библиотеке предоставить и несколько других подобных.

                    Я специально не хотел описывать этот кейс (он очень специфичный), а подать статью немного в другом ракурсе, как бы «вот библиотека, вот это она умеет». Но не фартануло… :)
              • 0
                > зарефлектить, к какому классу принадлежит данный метод, не представляется возможным
                Я правда не понимаю, в чем тут проблема. В каком порядке не применяй декораторы, все равно декоратор метода легко получает класса.

                def regular_dec(fn):
                    def wrapped(*args, **kwargs):
                        print 'regular decorator'
                        return fn(*args, **kwargs)
                    return wrapped
                
                def method_dec(fn):
                    def wrapped(self, *args, **kwargs):
                        print 'method decorator. class is', type(self)
                        return fn(self, *args, **kwargs)
                    return wrapped
                
                class A(object):
                    @regular_dec
                    @method_dec
                    def method1(self, some_arg=None):
                        print '>>> method1'
                
                    @method_dec
                    @regular_dec
                    def method2(self, some_arg=None):
                        print '>>> method2'
                
                A().method1()
                A().method2()
                


                regular decorator
                method decorator. class is <class '__main__.A'>
                >>> method1
                method decorator. class is <class '__main__.A'>
                regular decorator
                >>> method2
                


                Другое дело, что вы не получите этой информации извне. Но вы этого не узнаете и для любого другого метода, безотносительно декораторов. И ваша библиотека тут вроде бы никак не поможет.
                • 0
                  Смотрите мой комент выше
          • 0
            Приведите пожалуйста реальный пример с конкретными библиотеками и конкретными декораторами из этих библиотек.
            • +2
              Я не автор, но с _похожей_ бедой столкнулся при играх с bottle.py — у них декораторы @route() корёжат переданные аргументы (*args, **kwargs) и что-то там ещё так, что «внешний» декоратор вообще не вызывается. Например такой код:
              def my_deco(fn):
                def wrapped(*args, **kwargs):
                  print "HI"
                
                #....
              
              @my_deco
              @route("/")
              def index():
                #...
              

              Никогда не напечатает «HI».
              Есть штатный обходной путь, который приходится использовать с их же декоратором:
              @route("/", apply=[my_deco])
              def index():
                #...
              

              Первым желанием было сделать что-то наподобие сделанного автором топика, но потом — «отпустило», когда нагуглил таки про «apply». Отнюдь не в родной документации :)
              • 0
                Мдя… Спасибо большое за пример! Хорошо хоть в bottle.py штатный путь решения этой проблемы есть. Только вот очень уж не нравится мне такой подход авторов библиотек.
                • +1
                  Не за что!
                  Мне тоже такой подход не понравился, но другой «лёгкой» альтернативы пока не придумалось.
                  Самое неприятное, что этот метод нагуглился на каком-то форуме, чуть ли не в багтрекере :(

                  Собственно, было желание указывать шаблон через декоратор, как показано в примерах, ан — не вышло.

                  Замечу ещё, что порядок вызова декораторов из apply — LIFO:
                  @route("/", apply=[view("my_index_template"), my_beaker_cache])
                  def index():
                    return {
                       'data': some_data()
                    }
                  

                  Сначала сработает my_beaker_cache, закешировав отданный нами dict, а только потом вызовется шаблонизатор (и его никто кешировать не станет). Это может вызвать ошибки при отличии типа кеша от «memory» — в beaker сериализация идёт через json, который поддерживает ограниченное количество типов данных.
              • 0
                Конечно, не вызывается. Чтобы он вызвался, он должен идти «под» route, потому что route добавляет в dispatch нижележащего фреймворка функцию, которая передана ему в качестве параметра.
                • +1
                  Угу… Вот это и есть «нехорошее» т.е. не очевидное поведение.
                  И, кстати, вот такое не работает по причине не соответствия параметров функции:
                  @route("/abc")
                  @view("abcview")
                  def abc(db): # db - подключённая через Plugin база данных
                    # ...
                  

                  но прекрасно работает, когда вызовом занимается route.
                  В документации описания подобного поведения не нашёл. А нашёл слова, что, подключая plugin, функции, использующие параметр с указанным именем, автоматически его получают из плагина…

                  Возможно — плохо искал :)
                  • 0
                    Понятия не имею про плагины bottle.py. Краткий обзор и интуиция подсказывают мне, что у них что-то фатально сломано. То ли плагины, то ли view, то ли route.

                    Сами же декораторы работают вполне логично. Порядок их применения очевиден. С «регистрирующими» тоже все довольно очевидно — неявный стейт, все дела.
    • +1
      Тот же самый вопрос возник.
  • +9
    Для того чтобы внутри декоратора проверить не задекорирована ли декорируемая функции другим декоратором вы написали библиотеку декораторов, декорирующую декораторы вспомогательными декораторами.

    Я прав?
    • 0
      Правы. Вас это смущает? В конце концов я не агитирую использовать данную библиотеку именно в таком контексте. Можно на это, в конце-концов, посмотреть под другим углом. К примеру, вы ищете решение такой задачи — узнать какие методы в заданном классе задекорированы заданным декоратором. Данная библиотека позволяет задачу решить без парсинга исходного кода. Сам язык такого инструментария не дает. Это что, что-то совершенно бесполезное?
      • 0
        Но для этого нужно задекорировать ваши(или не ваши) декораторы вспомогательным декоратором из вашей библиотеки декораторов. А если в исходной сторонней библиотеке считается, что декоратор не задеркорирован (в конце-то концов, он же в той библиотеке, никто и не думал, что его возьмут, да извне задекорируют), что вы на это имеете сказать?
        • 0
          Я имею на это сказать. Почему по вашему он не ожидает, что он не задекорирован? Как он об этом знает? Вед декоратор только знает о функции, которую он декорирует. Я вед именно эту задачу и решаю — позволить себе знать не только вниз но и вверх. Если он об этом знает, значит он использует мою библиотеку? :)

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