装饰器将方法状态更改为函数

5 投票
4 回答
1352 浏览
提问于 2025-04-16 03:16

[更新]: 在问题下方的回答中有更新

我有一个检查程序,其中一个目标是让装饰器里的逻辑知道它装饰的函数是一个类方法还是普通函数。这个过程出现了一些奇怪的问题。下面是我在 Python 2.6 中运行的代码:

def decorate(f):
    print 'decorator thinks function is', f
    return f

class Test(object):
    @decorate
    def test_call(self):
        pass

if __name__ == '__main__':
    Test().test_call()
    print 'main thinks function is', Test().test_call

然后在执行时:

decorator thinks function is <function test_call at 0x10041cd70>
main thinks function is <bound method Test.test_call of <__main__.Test object at 0x100425a90>>

有没有人知道出了什么问题?@decorate 能否正确判断 test_call 是一个方法?

[回答] carl 的回答几乎是完美的。我在使用装饰器装饰一个子类的方法时遇到了问题。我调整了他的代码,加入了对父类成员的 im_func 比较:

ismethod = False
for item in inspect.getmro(type(args[0])):
    for x in inspect.getmembers(item):
        if 'im_func' in dir(x[1]):
            ismethod = x[1].im_func == newf
            if ismethod:
                break
    else:
        continue
    break

4 个回答

1

你的装饰器在函数变成方法之前就被执行了。在类里面用def关键字定义的函数,就像在其他地方定义的函数一样,都是函数。但是,当这些函数在类的主体中定义时,它们会被添加到类里,成为方法。装饰器在函数被类处理之前就已经运行了,这就是为什么你的代码会“失败”的原因。

@decorate装饰器无法知道这个函数实际上是一个方法。解决这个问题的一种方法是,不管这个函数是什么,都给它加上装饰(比如添加一个属性do_something_about_me_if_I_am_a_method ;-)),然后在类计算完成后再处理它(遍历类的成员,对那些被装饰的成员进行你想做的操作)。

3

不,这样做是不可能的,因为绑定方法和普通函数之间没有本质的区别。方法其实就是一个函数,只不过它会把调用它的实例作为第一个参数传进去(这在Python中是通过描述符实现的)。

像这样的调用:

Test.test_call

返回的是一个未绑定的方法,实际上可以理解为

Test.__dict__[ 'test_call' ].__get__( None, spam )

这也是一个未绑定的方法,尽管

Test.__dict__[ 'test_call' ]

是一个普通的函数。这是因为函数本身就是描述符,它们的 __get__ 方法会返回方法;当Python在查找链中遇到这样的函数时,它会调用 __get__ 方法,而不是继续向上查找。

实际上,函数的“绑定状态”是在运行时决定的,而不是在定义时决定的!

装饰器看到的只是函数本身的定义,而不会在 __dict__ 中查找,所以它无法判断这个函数是否是一个绑定的方法。


可能可以通过一个类装饰器来修改 __getattribute__ 来实现这个功能,但这是一种特别复杂的黑科技。你为什么一定要有这个功能呢?其实既然你必须自己在函数上加装饰器,那你可以传递一个参数,告诉它这个函数是否是在类里面定义的,不是更简单吗?

class Test:
    @decorate( method = True )
    def test_call:
        ...

@decorate( method = False )
def test_call:
    ...
5

正如其他人所说,函数在绑定之前就已经被装饰了,所以你不能直接判断它是“方法”还是“函数”。

判断一个函数是否是方法的一个合理方法是检查它的第一个参数是否是'self'。虽然这个方法并不是绝对可靠,但大多数Python代码都遵循这个约定:

import inspect
ismethod = inspect.getargspec(method).args[0] == 'self'

这里有一种复杂的方法,它似乎能自动判断一个方法是否被绑定。这个方法在CPython 2.6上对一些简单的情况有效,但不能保证总是有效。它会判断一个函数是否是方法,如果第一个参数是一个对象,并且这个对象上绑定了这个装饰过的函数。

import inspect

def decorate(f):
    def detect(*args, **kwargs):
        try:
            members = inspect.getmembers(args[0])
            members = (x[1].im_func for x in members if 'im_func' in dir(x[1]))
            ismethod = detect in members
        except:
            ismethod = False
        print ismethod

        return f(*args, **kwargs)
    return detect

@decorate
def foo():
    pass

class bar(object):
    @decorate
    def baz(self):
        pass

foo() # prints False
bar().baz() # prints True

撰写回答