【问题标题】:Create decorator with arguments that doesn't modify the function使用不修改函数的参数创建装饰器
【发布时间】:2018-05-26 18:39:22
【问题描述】:

编辑:装饰器应该在运行时对函数做一些事情,而不是修改它。

我希望能够在函数顶部添加一个装饰器,以便在脚本开始时使用参数执行,并打印结果(作为示例)。

类似于下面的代码,除了下面的代码不起作用,因为我不知道将参数传递给它的简洁方法。我不想修改原始函数,只是在解释函数时将其作为“测试”部分。

def test_decorator(func, *args, **kwargs):  # I'm aware this doesn't work. I'm modeling it after class methods for simplicity sake.
    print("Passed:", args, kwargs,
          "\nResult:", func(*args, **kwargs))

    return func


@test_decorator(70, 20, 10, "last_arg_example:")
def test_example(arg1, arg2, arg3=None, arg4="arg4_example:"):
    print(arg4, arg1 + arg2 + (arg3 if arg3 else 0))

我知道装饰器定义的第一行搞砸了,但我也不清楚如何修复它。我能找到的所有东西都使用某种“包装器”,但我不想修改原始文件,就像我说的那样。

如果函数不需要参数,我也应该能够在它上面只用@test_decorator 来装饰它。或者即使它确实需要参数,也不要对实际函数执行任何操作并完全打印其他内容。

我该怎么做?

编辑: 为了更简洁,我希望装饰器执行它后面的函数,并将参数传递给装饰器并打印输出。

>>> @decorator(4, 5)
>>> def test1(arg1, arg2):
...    return arg1 + arg2
...
9
>>>

进一步的功能是通过不向函数传递任何参数来处理不带参数。这里有两种情况:

>>> @decorator
>>> def test2(arg1=4, arg2=5):
...    return arg1 + arg2
...
9
>>>

如果参数没有默认值:

>>> @decorator
>>> def test1(arg1, arg2):
...    return arg1 + arg2
...
Traceback (most recent call last):
 File "<stdin>", line 1 in <module>
TypeError: test1() takes exactly 2 arguments (0 given)
>>>

请记住,我知道交互式解释器不是这样工作的。我实际上并没有尝试这些示例;然后我打了出来。无论如何我都无法测试这些示例,因为我没有适合它们的装饰器。

【问题讨论】:

  • 现在听起来您希望装饰器调用被定义的函数 as 它正在被装饰。将参数传递给装饰器...正确吗?
  • @martineau 是的,正确的。亦不修改原文,重述以备。
  • 好的,在装饰器中调用被装饰函数的结果应该如何处理?
  • @martineau 只需打印结果,然后可能会将其记录到列表中。这个装饰器将在一个实用程序文件中定义,所以我想知道除了函数定义上方的行之外,不修改任何内容的“最佳”方法是创建结果注册表。
  • spikespaz:好的,请参阅下面my answer 的最新更新。

标签: python python-3.x function scope decorator


【解决方案1】:

@martineau 的回答涵盖了如何制作一个接受参数并返回适当装饰器的装饰器工厂。如果您使用这种模式,您的实际装饰器可以返回未修改的原始函数:

def test_decorator(*args, **kwargs):
    def decorator(func):
        return func
    return decorator

与原始规范的唯一区别是您必须在无参数情况下显式调用工厂:@test_decorator() 而不仅仅是 @test_decorator

现在您可以在工厂或装饰器中做任何您想做的事情,包括在某处注册函数调用。例如:

call_list = []

def test_decorator(*args, **kwargs):
    def decorator(func):
        def make_call():
            return func(*args, **kwargs)
        call_list.append(make_call)
        # Decorated function is completely unchanged
        return func
    return decorator

@test_decorator(70, 20, 10, "last_arg_example:")
@test_decorator(60, 10, 20, "middle_arg_example:")
@test_decorator(50, 0, 30, "first_arg_example:")
def test_example(arg1, arg2, arg3=None, arg4="arg4_example:"):
    print(arg4, arg1 + arg2 + (arg3 if arg3 else 0))

for item in call_list:
    item()

这会在第一次导入模块时在call_list 中注册每个修饰函数调用,然后一个接一个地运行修饰调用。它不会以任何方式修改实际的装饰函数。另一个不错的副作用是您可以任意嵌套工厂次数,如图所示。每个内部装饰的返回值都传递给外部装饰,这意味着注册函数调用的顺序是“向后”。上面的结果是:

first_arg_example: 80
middle_arg_example: 90
last_arg_example: 100

更多关于类装饰器的信息

您可以将装饰器工厂重新格式化为一个类。装饰器(出于您的目的)是任何接受函数并返回函数的可调用对象。这意味着具有__call__ 方法的类的实例也可以是装饰器。上面的例子可以改写为

call_list = []

class TestDecorator:
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def __call__(self, func):
        def make_call():
            return func(*self.args, **self.kwargs)
        call_list.append(make_call)
        # Decorated function is completely unchanged
        return func

@TestDecorator(70, 20, 10, "last_arg_example:")
@TestDecorator(60, 10, 20, "middle_arg_example:")
@TestDecorator(50, 0, 30, "first_arg_example:")
def test_example(arg1, arg2, arg3=None, arg4="arg4_example:"):
    print(arg4, arg1 + arg2 + (arg3 if arg3 else 0))

for item in call_list:
    item()

【讨论】:

  • 正是我想要的。另一个不错的功能是让它不接受括号。这是否违反了pythonic规则?这样做与使用 __init____call__ 的类有什么区别?我也遇到过一些地方。
  • @spikepaz。没有大的区别,只是类而不是函数形式。阅读起来更容易一些,因为您不必遍历嵌套的命名空间。
  • 现在过渡到类装饰器形式会很困难吗?如果这更具可读性并且易于转换,我想这样做。不太确定如何,但如果我并排看到函数形式和类形式,我相信我会建立联系。
  • 这就是关于括号的事情。外部工厂必须根据输入来决定是返回适当装饰器的工厂还是装饰器本身。实际上并不难:if not kwargs and len(args) == 1 and callable(args[0]):。但是现在你永远不能装饰你传递一个可调用对象的一个​​参数的函数。另外,使用括号更加一致,因为它提醒您正在设置函数调用,而不是真正装饰函数。
  • 很公平。我想在此基础上坚持括号会更好。班级形式呢?应该这样做吗?
【解决方案2】:

你的意思是这样的吗?当你传递装饰器函数参数时,它必须返回一个“普通”装饰器——一个只接受原始函数作为其参数的装饰器。显式传递一个或多个参数的装饰器实际上是一个“装饰器工厂”函数,它返回实际的装饰器,然后我们将其应用于以下函数定义。

据我所知,您的装饰器不使用它的任何自己的 参数——70, 20, 10, "last_arg_example:" 值——所以在下面的代码中它们只是被打印出来然后传递给原始函数来获取使用它们的结果,这些结果也被打印出来。因此,在所有这些 disclimers 不碍事的情况下,这里有一个示例来说明我所概述的内容:

def test_decorator(*deco_args, **deco_kwargs):

    def decorator(func):
        print('Decorator-factory called: {}({}, {})'.format(
            func.__name__,
            ', '.join(repr(arg) for arg in deco_args),
            ', '.join('{}={!r}'.format(k, v) for k, v in deco_kwargs)))

        results = func(*deco_args, **deco_kwargs)
        print("    function returned: {!r}".format(results))

        def wrapped(*args, **kwrds):  # A no-op decoration.
            return func(*args, **kwrds)

        return wrapped

    return decorator

print('Defining decorated test_example() function:')
@test_decorator(70, 20, 10, "last_arg_example:")
def test_example(arg1, arg2, arg3=None, arg4="arg4_example:"):
    print(arg4, arg1 + arg2 + (arg3 if arg3 else 0))

print()
print('Calling decorated test_example() function:')
test_example('arg1', 'arg2', arg3='Not None')

输出:

Defining decorated test_example() function:
Decorator-factory called: test_example(70, 20, 10, 'last_arg_example:', )
last_arg_example: 100
    function returned: None

Calling decorated test_example() function:
arg4_example: arg1arg2Not None

【讨论】:

  • @Mad: 很好的捕获...很容易修复(因为它不像其他两个那样被使用)。谢谢!
  • 我认为 OP 希望外部函数调用将函数调用添加到某处的注册表中,而不是实际装饰它。无论哪种方式,+1 用于展示如何传递参数。
  • @Mad:再次感谢...在这种情况下,我建议您发布自己的答案。
  • 当然,我想我会的。
  • @spikespaz。我已经发布了另一个答案,解决了我认为您关心的问题。
猜你喜欢
  • 1970-01-01
  • 2020-06-20
  • 2012-07-28
  • 2015-09-13
  • 2018-05-24
  • 2021-07-27
  • 1970-01-01
  • 2022-08-15
  • 1970-01-01
相关资源
最近更新 更多