如何在装饰器中区分方法和函数?

14 投票
4 回答
1921 浏览
提问于 2025-04-15 20:22

我想写一个装饰器,它的行为会根据是应用在函数上还是方法上而有所不同。

def some_decorator(func):
    if the_magic_happens_here(func): # <---- Point of interest
        print 'Yay, found a method ^_^ (unbound jet)'
    else:
        print 'Meh, just an ordinary function :/'
    return func

class MyClass(object):
    @some_decorator
    def method(self):
        pass

@some_decorator
def function():
    pass

我试过了 inspect.ismethod()inspect.ismethoddescriptor()inspect.isfunction(),但是都没有成功。问题在于,当方法在类内部被访问时,它实际上既不是绑定方法也不是未绑定方法,而是一个普通的函数。

我真正想做的是把装饰器的动作延迟到类被实例化的时候,因为我需要在实例的范围内调用这些方法。为此,我想给方法加一个属性,然后在 MyClass.__new__() 方法被调用时,去查找这些属性。这个装饰器需要作用的类必须继承自一个我可以控制的类。你可以利用这一点来解决问题。

对于普通函数来说,延迟是没有必要的,装饰器应该立即生效。这就是我想区分这两种情况的原因。

4 个回答

1

你是否需要在选择返回哪个包装器时让事情变得神奇,还是可以等到函数真正被调用时再让它变得神奇?

你可以尝试给你的装饰器加一个参数,来指明应该使用哪一个包装器,比如:

def some_decorator( clams ):
   def _mydecor(func ):
       @wraps(func)
       def wrapping(*args....)
          ...
       return wrapping
   def _myclassdecor(func):
       @wraps(func)
       .....

   return _mydecor if clams else _myclassdecor

另外,我还建议你可以创建一个元类,并在这个元类中定义init方法,去查找那些被你的装饰器装饰的方法,并相应地进行修改,就像Alex提到的那样。使用这个元类作为你的基类,因为所有使用这个装饰器的类都会从基类继承,所以它们也会得到这个元类的类型,并使用它的init方法。

16

从Python 3.3开始,使用了一个叫做PEP 3155的规范:

def some_decorator(func):
    if func.__name__ != func.__qualname__:
        print('Yay, found a method ^_^ (unbound jet)')
    else:
        print('Meh, just an ordinary function :/')
    return func

在这个规范中,类中的一个方法x的名字会显示为A.x,而一个普通的函数x的名字则只会显示为x

12

我会依赖一个约定:那些会变成方法的函数,第一个参数叫做 self,而其他函数则没有这个要求。这种方法虽然不太可靠,但目前没有更好的办法。

所以(伪代码,因为我在这里用注释代替你想做的事情...):

import inspect
import functools

def decorator(f):
  args = inspect.getargspec(f)
  if args and args[0] == 'self':
     # looks like a (future) method...
  else:
     # looks like a "real" function
     @functools.wraps(f)
     def wrapper  # etc etc

为了让这个方法更可靠一点,假设所有相关的类都继承自一个你控制的类,可以让这个类提供一个元类(这个元类也会被这些类继承),它会在类体结束时检查一些东西。可以通过 wrapper._f = f 让被包装的函数可访问,元类的 __init__ 方法可以检查所有被包装的方法的第一个参数是否确实是 self

不过,不幸的是,没有简单的方法来检查其他函数(非未来方法)在被包装时是否没有这样的第一个参数,因为在这种情况下你无法控制环境。装饰器可以通过 f_globals(全局字典,也就是模块的字典)和 f_name 属性来检查“顶层”函数(那些在模块中直接定义的函数)——如果这个函数是全局的,推测它不会被后续赋值为类的属性(这样就会变成未来方法;-),所以如果有 self 作为第一个参数,就可以判断是错误的并发出警告(同时仍然把这个函数当作真正的函数;-)。

另一种选择是在装饰器内部进行装饰,假设这是一个真正的函数,同时也将原始函数对象作为 wrapper._f 提供。然后,元类的 __init__ 可以重新对它看到的所有被标记的类体中的函数进行装饰。这种方法比我刚才提到的依赖约定的方法要可靠得多,即使有额外的检查。尽管如此,像

class Foo(Bar): ... # no decorations

@decorator
def f(*a, **k): ...

Foo.f = f   # "a killer"... function becomes method!

这样的情况仍然会有问题——你可以尝试在元类中用 __setattr__ 来拦截这个,但在 class 语句之后对类属性的其他赋值可能会变得麻烦。

用户的代码越自由(而且 Python 通常给程序员很多这样的自由),你的“框架式”代码就越难以保持严格控制,这当然是个挑战;-)。

撰写回答