Как применять декоратор класса у всех декораторов по методам

Я использую этот способ оформления всех методов

import inspect

def decallmethods(decorator, prefix='test_'):
  def dectheclass(cls):
    for name, m in inspect.getmembers(cls, inspect.ismethod):
      if name.startswith(prefix):
        setattr(cls, name, decorator(m))
    return cls
  return dectheclass


@decallmethods(login_testuser)
class TestCase(object):
    def setUp(self):
        pass

    def test_1(self):
        print "test_1()"

    def test_2(self):
        print "test_2()"

Это работает, но оно применяется сверху, если у меня есть другие декораторы.

Я имею в виду

Теперь результат

@login_testuser
@other
def test_2(self):
    print "test_2()"

Но я хочу

@other
@login_testuser
def test_2(self):
    print "test_2()"

Ответ 1

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

def operation(a, b):
    print('doing operation')
    return a + b

Просто он сделает это

>>> hi = operation('hello', 'world')
doing operation
>>> print(hi)
helloworld

Теперь определите декоратор, который печатает что-то до и после вызова его внутренней функции (эквивалент декодера other, который вы хотите украсить позже):

def other(f):  
    def other_inner(*a, **kw):
        print('other start')
        result = f(*a, **kw)
        print('other finish')
        return result
    return other_inner

При этом создайте новую функцию и декоратор

@other
def o_operation(a, b):
    print('doing operation')
    return a + b

Помня, это в основном эквивалентно o_operation = other(operation)

Запустите это, чтобы убедиться, что он работает:

>>> r2 = o_operation('some', 'inner')
other start
doing operation
other finish
>>> print(r2)
someinner

Наконец, окончательный декоратор, который вы хотите вызвать непосредственно перед operation, но не d_operation, но с вашим существующим кодом он приводит к следующему:

def inject(f):
    def injected(*a, **kw):
        print('inject start')
        result = f(*a, **kw)
        print('inject finish')
        return result
    return injected

@inject
@other
def i_o_operation(a, b):
    print('doing operation')
    return a + b

Запустите выше:

>>> i_o_operation('hello', 'foo')
inject start
other start
doing operation
other finish
inject finish
'hellofoo'

Как упоминалось, декораторы действительно закрыты и, следовательно, поэтому возможно иметь внутри внутри элементы, которые фактически инсценируются внутри. Вы можете связаться с ними, пройдя через атрибут __closure__:

>>> i_o_operation.__closure__
(<cell at 0x7fc0eabd1fd8: function object at 0x7fc0eabce7d0>,)
>>> i_o_operation.__closure__[0].cell_contents
<function other_inner at 0x7fc0eabce7d0>
>>> print(i_o_operation.__closure__[0].cell_contents('a', 'b'))
other start
doing operation
other finish
ab

Посмотрите, как это эффективно вызывает функцию внутри закрытия injected напрямую, как если бы она была развернута. Что делать, если это закрытие может быть заменено на ту, которая сделала инъекцию? Для всей нашей защиты __closure__ и cell.cell_contents доступны только для чтения. Что нужно сделать, так это построить совершенно новые функции с предполагаемыми закрытиями, используя конструктор функции FunctionType (найденный в types)

Вернемся к проблеме. Поскольку мы имеем сейчас:

i_o_operation = inject(other(operation))

И мы хотим

o_i_operation = other(inject(operation))

Нам действительно нужно как-то отключить вызов other от i_o_operation и каким-то образом обернуть его с помощью inject, чтобы создать o_i_operation. (Драконы следуют после разрыва)


Сначала создайте функцию, которая эффективно вызывает inject(operation), заставив замыкание на уровень глубоко (так что f будет содержать только оригинальный вызов operation), но смешайте его с кодом, созданным inject(f):

i_operation = FunctionType(
    i_o_operation.__code__,
    globals=globals(),
    closure=i_o_operation.__closure__[0].cell_contents.__closure__,
) 

Так как i_o_operation является результатом inject(f), мы можем взять этот код для создания новой функции. globals - формальность, которая требуется, и, наконец, берет замыкание вложенного уровня, и создается первая часть функции. Убедитесь, что other не вызывается.

>>> i_operation('test', 'strip')
inject start
doing operation
inject finish
'teststrip'

Ухоженная. Однако мы все еще хотим, чтобы other был завернут за пределами этого, чтобы наконец создать o_i_operation. Нам нужно каким-то образом поставить эту новую функцию, которую мы создали в закрытии, и способ сделать это - создать суррогатную функцию, которая создает один

def closure(f):
    def surrogate(*a, **kw):
        return f(*a, **kw)
    return surrogate

И просто используйте его для построения и извлечения нашего закрытия

o_i_operation = FunctionType(
    i_o_operation.__closure__[0].cell_contents.__code__,
    globals=globals(),
    closure=closure(i_operation).__closure__,
)

Вызов:

>>> o_i_operation('job', 'complete')
other start
inject start
doing operation
inject finish
other finish
'jobcomplete'

Похоже, мы наконец получили то, что нам нужно. Хотя это точно не соответствует вашей точной проблеме, это началось с правильной дорожки, но уже довольно волосатое.


Теперь для актуальной проблемы: функция, которая обеспечит, чтобы функция декоратора была самой внутренней (окончательной), вызываемой перед заданной оригинальной, неупорядоченной функцией - то есть для данного target и f(g(...(callable)), мы хотим эмулировать результат, который дает f(g(...(target(callable)))). Это код:

from types import FunctionType

def strip_decorators(f):
    """
    Strip all decorators from f.  Assumes each are functions with a
    closure with a first cell being the target function.
    """

    # list of not the actual decorator, but the returned functions
    decorators = []
    while f.__closure__:
        # Assume first item is the target method
        decorators.append(f)
        f = f.__closure__[0].cell_contents
    return decorators, f

def inject_decorator(decorator, f):
    """
    Inject a decorator to the most inner function within the stack of
    closures in `f`.
    """

    def closure(f):
        def surrogate(*a, **kw):
            return f(*a, **kw)
        return surrogate

    decorators, target_f = strip_decorators(f)
    result = decorator(target_f)

    while decorators:
        # pop out the last one in
        decorator = decorators.pop()
        result = FunctionType(
            decorator.__code__,
            globals=globals(),
            closure=closure(result).__closure__,
        )

    return result

Чтобы проверить это, мы используем типичный пример use-case-html-тегов.

def italics(f):
    def i(s):
        return '<i>' + f(s) + '</i>'
    return i

def bold(f):
    def b(s):
        return '<b>' + f(s) + '</b>'
    return b

def underline(f):
    def u(s):
        return '<u>' + f(s) + '</u>'
    return u

@italics
@bold
def hi(s):
    return s

Запуск теста.

>>> hi('hello')
'<i><b>hello</b></i>'

Наша цель - вставить декоратор underline (в частности, u(hi) вызываемый) в самое внутреннее закрытие. Это можно сделать так, используя указанную выше функцию:

>>> hi_u = inject_decorator(underline, hi)
>>> hi_u('hello')
'<i><b><u>hello</u></b></i>'

Работает с невыделенными функциями:

>>> def pp(s):
...     return s 
... 
>>> pp_b = inject_decorator(bold, pp)
>>> pp_b('hello')
'<b>hello</b>'

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

def prefix(p):
    def decorator(f):
        def inner(*args, **kwargs):
            new_args = [p + a for a in args]
            return f(*new_args, **kwargs)
        return inner
    return decorator

Пример использования:

>>> @prefix('++')
... def prefix_hi(s):
...     return s
... 
>>> prefix_hi('test')
'++test'

Теперь попробуйте вставить декоратор bold, например:

>>> prefix_hi_bold = inject_decorator(bold, prefix_hi)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 18, in inject_decorator
ValueError: inner requires closure of length 2, not 1

Это просто потому, что замыкание, образованное decorator внутри prefix, имеет два элемента, одно из которых является префиксной строкой p, а вторая является фактической функцией, а inner - внутри, которая ожидает, что оба присутствовать внутри его закрытия. Решение, которое потребует большего количества кода для анализа и восстановления деталей.


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

Если вы хотите превратить inject_decorator в декоратор и/или смешать его в своем декораторе, удачи, большая часть тяжелой работы уже выполнена.