将带参数的Python装饰器合并为一个装饰器

19

我正在使用两个不同库中的两个不同装饰器。假设它们是:@decorator1(param1, param2)@decorator2(param3, param4)。我在许多函数中经常使用它们,例如:

from moduleA import decorator1
from moduleB import decorator2

@decorator2(foo='param3', bar='param4')
@decorator1(name='param1', state='param2')
def myfunc(funcpar1, funcpar2):
    ...

既然这种情况每次都会发生,我想创建一个自定义装饰器来处理它们两个。类似于:

@mycustomdecorator(name='param1', state='param2',
                   foo='param3', bar='param4')
def myfunc(funcpar1, funcpar2):
    ...

我要怎样做才能实现这个目标?


1
我认为这不是一样的,在你提到的帖子中,@customdecorator 接收装饰器。但实际情况并非如此。 - Lin
你可以轻松地扩展这个答案,以添加参数。 - Aran-Fey
我认为我们不应该为具有和不具有参数的合并装饰器设置两个单独的问答,因此我将在另一个问题中发布更完整的答案。 - Aran-Fey
@Aran-Fey 我同意你的观点。 - Lin
我认为这个重复标志不准确。另一个问题没有询问带参数的装饰器,而且被接受的答案也不是这个问题的答案。相关?当然。重复?不是。 - Gloweye
显示剩余4条评论
3个回答

20
我认为你不应该这样做——使用原始名称可以使修饰符更易读。但是,如果你真的想这样做,可以按照以下方式进行:
import functools

from moduleA import decorator1
from moduleB import decorator2

def my_decorator(foo, bar, name, state):
    def inner(func):
        @decorator2(foo=foo, bar=bar)
        @decorator1(name=name, state=state)
        @functools.wraps(func)  # Not required, but generally considered good practice
        def newfunc(*args, **kwargs)
            return func(*args, **kwargs)
        return newfunc
    return inner

@my_decorator(foo='param3', bar='param4', name='param1', state='param2')
def myfunc(funcpar1, funcpar2):
    ...

根据评论,这里提供一种替代方法:

def my_decorator(foo, bar, name, state):
    def inner(func):
        # Please note that for the exact same result as in the other one, 
        # the order of decorators has to be reversed compared to normal decorating
        newfunc = decorator1(name=name, state=state)(func)
        newfunc = decorator2(foo=foo, bar=bar)(newfunc)
        # Note that functools.wraps shouldn't be required anymore, as the other decorators should do that themselves
        return newfunc
    return inner

对于一些人来说,这可能看起来更简单。但是,熟悉Python的人习惯于使用@应用装饰器——仅仅因为这个原因,我更喜欢第一个选项。我知道我需要花三倍的时间来第一次阅读并理解它所做的事情。
实际上很简单——只需编写一个返回另一个装饰器的装饰器,该装饰器将具有其内部函数由其他两个装饰器装饰的效果;)
为了养成良好的习惯,使用functools.wraps也是一个好主意。它是标准库,并且有助于调试和交互式控制台使用:https://docs.python.org/3.7/library/functools.html 总的来说,我认为多出来的一行代码完全值得使用分开的装饰器带来的清晰度。当你在三个月后再次阅读自己的代码时,你会感激自己的。

1
你应该使用 functools.wraps。如果你尝试 print(myfunc),你会得到 <function my_decorator.<locals>.inner.<locals>.newfunc at 0x7f5ee7bbee18> 的输出。 - Aran-Fey
1
虽然我通常认为节省一两行代码并不值得牺牲可读性,但如果我需要将同样的两个装饰器应用于多个函数,我肯定会想知道这是否是偶然的。在最后一种情况下,我会重构整个代码以避免重复。 - bruno desthuilliers
如果它们来自两个不同的库,那就不是偶然的了。@Aran-Fey:哎呀。这会教训我编辑得太快了。 - Gloweye
@Jacco 但是那么我就开始想起来为什么我从来没有看到这些库的作者在同一个房间里出现过。 - Mad Physicist
我自己也曾经处理过类似的情况 - 例如,将PyQt信号/插槽与硬件控制器上的状态条件相结合。有很多重复的装饰器。话虽如此,如果您自己编写了这两个库中的任何一个,那么将它们合并成一个装饰器可能是值得的。我知道我会这样做。 - Gloweye
显示剩余5条评论

4

这只是简单的函数组合,其中decorator1decorator2返回要组合的函数。真正的工作可以抽象为一个compose函数。

# In the special case of composing decorators, the lambda expression
# only needs to be defined with a single argument, like
#
#   lambda func: f(g(func))
#
# but I show a more general form.
def compose(f, g):     
    return lambda *args, **kwargs: f(g(*args, **kwargs))

def customdecorator(foo, bar, name, state):
    return compose(decorator2(foo=foo, bar=bar),
                   decorator1(name=name, state=state))

2

装饰器只不过是一种语法糖,就像 func = decorator(func) 这样的语法。

因此,您可以使用以下语法轻松创建自己的装饰器,以实现您想要的任何功能:

def mycustomdecorator(foo, bar, name, state)
    def innerdecorator(func):
        func = decorator1(foo=foo, bar=bar)(func)
        func = decorator2(name=name, state=state)(func)
        return func
    return innerdecorator

之后,您应该能够毫不费力地使用@mycustomdecorator。如果有效,请告诉我,我没有测试过,但从理论上讲应该可以。

其中的奥妙在于:首先,我们需要检索装饰器的参数。这样,我们就能将它们传递到嵌套函数中。然后,我们将我们的函数作为参数接受,最后,我们获取我们函数的参数。我们可以根据需要嵌套我们的def-s。


请您详细说明一下,好吗? - Дмитрий Клименко
函数的数量没问题;只是decorator1decorator2被调用的位置不对。你想在定义wrapper之前在innerdecorator中应用它们,这样每次调用func时就不会重复调用它们了。 - chepner
谢谢chepner,你说得完全正确!我会记住这个以备将来使用。 - Дмитрий Клименко
@Aran-Fey,你能否分享一个例子,说明我们如何不使用额外的嵌套函数? - Дмитрий Клименко
1
错误的部分在于 wrapper 应该调用func,而不是返回它。 - chepner
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接