【问题标题】:How to apply class decorator at base of all decorators on methods如何在方法的所有装饰器的基础上应用类装饰器
【发布时间】:2016-01-19 20:55:34
【问题描述】:

我用这种方式装饰所有方法

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()"

【问题讨论】:

  • 您可能想考虑如何将代码从test_2 = login_testuser(other(test_2)) 转换为test_2 = other(login_testuser(test_2))。我的建议是您考虑另一种方法来避免测试中的 other 装饰器。虽然有一些方法可以操纵装饰器形成的闭包,但它会很混乱。
  • @metatoaster 。你能给我一些想法我怎么能在闭包中做到这一点。我有很多方法,所以不可能复制它。
  • 我建议您编辑标题以反映您正在做的事情的真实意图,例如“将装饰器注入到已经装饰的原始函数中”。或类似的东西。事实证明这比我想象的要难得多。

标签: python python-decorators


【解决方案1】:

这肯定是一个的想法,但是你想做的事情在某种程度上是可以做到的,这需要很多时间来解释。首先,不要将装饰器视为语法糖,而是将它们视为它们的真正本质:一个函数(即闭包),其中存在一个函数。现在这不碍事了,假设我们有一个函数:

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))

我们实际上必须以某种方式从i_o_operation 中剥离对other 的调用,并以某种方式用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_operationinject(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'

看起来我们终于得到了我们需要的东西。虽然这并不能完全回答您的确切问题,但这开始是正确的,但已经很麻烦了。


现在解决实际问题:一个函数将确保装饰器函数在给定的原始未装饰函数之前是最内部(最终)可调用的 - 即对于给定的targetf(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

为了测试这一点,我们使用一个典型的示例用例 - 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

这仅仅是因为decoratorprefix 中形成的闭包有两个元素,一个是前缀字符串p,第二个是实际函数,而inner 嵌套在里面,期望这两个元素存在于其封闭内。解决这个问题需要更多代码来分析和重构细节。


无论如何,这个解释花费了相当多的时间和文字,所以我希望你能理解这一点,也许能让你开始走上真正的正轨。

如果你想将inject_decorator 变成一个装饰器,和/或将它混入你的类装饰器中,祝你好运,大部分艰苦的工作已经完成。

【讨论】:

  • 非常感谢您的解释,我明白了:)
猜你喜欢
  • 2020-11-30
  • 2020-01-11
  • 2014-01-14
  • 2011-10-05
  • 2019-02-03
相关资源
最近更新 更多