装饰函数并增加功能,保持参数数量不变

2 投票
4 回答
603 浏览
提问于 2025-04-15 23:18

我想给一个函数加上装饰器,想用这样的方式:

def deco(func):
    def wrap(*a,**kw):
        print "do something"
        return func(*a,**kw)
    return wrap

问题是,如果被装饰的函数有这样的原型:

def function(a,b,c): return

当加上装饰器后,原型会被可变参数搞坏,比如调用 function(1,2,3,4) 时不会报错。有没有办法避免这个问题呢?我该如何定义一个和被装饰函数(func)原型相同的包装函数?

这样做是不是从概念上就有问题呢?

编辑

我有个奇怪的想法,就是想在不改变函数签名的情况下,轻松调用“父方法”。就像这样:

def __init__(self, something)
    super(ClassName, self).__init__(something)

变成:

@extended
def __init__(self, something):
    ...

我在想这样做是否可行,以及这样做是否有意义。

编辑

正如Alex指出的,下面的代码不会抛出异常:

function(1,2,3,4)

4 个回答

3

这里有一个小技巧,首先从被装饰的函数中获取原始的参数说明,然后通过评估一个包含相同参数的字符串来创建一个lambda函数。接着,这个装饰器就被包裹在这个lambda函数里,这样对外界来说,它的参数名称和默认值看起来是一样的:

import inspect, time
import functools

def decorator_wrapper(old_function, new_function):
    args, arglist, kw, default = inspect.getargspec(old_function)
    args = list(args)

    if arglist:
       args.append(arglist)

    if kw:
       args.append(kw)

    callstring = inspect.formatargspec(args, arglist, kw, default, formatvalue=lambda value: "")
    argstring = inspect.formatargspec(args, arglist, kw, default)[1:-1]

    unique_name = "_func" + str(int(time.time()))
    codestring = "lambda " + argstring + " : " + unique_name + callstring
    decorated_function = eval(codestring, {unique_name: new_function})

    return functools.wraps(old_function)(decorated_function)
4

你说“调用函数(1,2,3,4)不会导致异常”是错的。看看这个:

>>> def deco(f):
...   def w(*a, **k):
...     print 'do something'
...     return f(*a, **k)
...   return w
... 
>>> def f(a, b, c): return
... 
>>> f(1, 2, 3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: f() takes exactly 3 arguments (4 given)
>>> decorated = deco(f)
>>> decorated(1, 2, 3, 4)
do something
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<stdin>", line 4, in w
TypeError: f() takes exactly 3 arguments (4 given)

正如你所看到的,调用没有装饰的 f 时,你会得到完全相同的异常(虽然是在你添加的 print 之后)。

为了保留被包装函数的元数据(比如名字、文档字符串),可以使用 functools.wraps。预测被包装的函数在调用时会抛出异常(为了避免在调用之前做其他工作)总是很难(一般来说是“不可能”的,因为这和停机问题是等价的;在特定情况下,如果你只关心参数名称和数量不匹配时抛出类型错误,并想把这个特殊的异常情况和其他异常情况区别对待,那就“只是难”了——这是个特别的要求;-0)。

如果你坚持认为你绝对需要这个(也许能解释一下为什么吗?),我会很乐意(好吧,不是乐意,但我会咬牙坚持做的,毕竟我面前有个长周末;-)带你走这条复杂的道路。

3

decorator模块可以帮助你创建一个装饰器,这个装饰器能够保持函数的签名不变。

这样,当你调用这个函数时,就能得到你预期的异常,而且使用inspect.getargspec时也能得到正确的函数签名。

它的工作原理是动态构建一个函数定义,并使用exec来执行。不幸的是,目前没有更简单的内置方法可以做到这一点。

撰写回答