Python 装饰器让函数忘记其所属类

64 投票
9 回答
28465 浏览
提问于 2025-04-11 18:36

我正在尝试写一个装饰器来进行日志记录:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    @logger
    def f():
        pass

C().f()

我希望它能打印出:

Entering C.f

但实际上我得到了这个错误信息:

AttributeError: 'function' object has no attribute 'im_class'

我想这可能跟'logger'里面的'myFunc'的作用域有关,但我不知道具体是什么。

9 个回答

18

这里提出的想法都很不错,但也有一些缺点:

  1. inspect.getouterframesargs[0].__class__.__name__ 不适用于普通函数和静态方法。
  2. __get__ 必须在一个类里面,这被 @wraps 拒绝了。
  3. @wraps 本身应该更好地隐藏装饰器的痕迹。

所以,我结合了这个页面上的一些想法、链接、文档和我自己的思考,
最终找到了一个解决方案,解决了以上三个缺点。

结果就是 method_decorator

  • 知道被装饰的方法绑定到哪个类。
  • functools.wraps() 更准确地隐藏装饰器的痕迹。
  • 对绑定和未绑定的实例方法、类方法、静态方法和普通函数进行了单元测试。

使用方法:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

查看 完整的单元测试以获取使用细节

这里是 method_decorator 类的代码:

class method_decorator(object):

    def __init__(self, func, obj=None, cls=None, method_type='function'):
        # These defaults are OK for plain functions
        # and will be changed by __get__() for methods once a method is dot-referenced.
        self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type

    def __get__(self, obj=None, cls=None):
        # It is executed when decorated func is referenced as a method: cls.func or obj.func.

        if self.obj == obj and self.cls == cls:
            return self # Use the same instance that is already processed by previous call to this __get__().

        method_type = (
            'staticmethod' if isinstance(self.func, staticmethod) else
            'classmethod' if isinstance(self.func, classmethod) else
            'instancemethod'
            # No branch for plain function - correct method_type for it is already set in __init__() defaults.
        )

        return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts.
            self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func.

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __getattribute__(self, attr_name): # Hiding traces of decoration.
        if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__().
            return object.__getattribute__(self, attr_name) # Stopping recursion.
        # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, e.g.: __module__, __class__, __name__, __doc__, im_*, func_*, etc.
        return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func.

    def __repr__(self): # Special case: __repr__ ignores __getattribute__.
        return self.func.__repr__()
30

函数在运行时才会变成方法。也就是说,当你调用 C.f 时,你得到的是一个绑定的函数(并且 C.f.im_class 是 C)。在你定义这个函数的时候,它只是一个普通的函数,并没有绑定到任何类上。这个没有绑定、没有关联的函数就是被 logger 装饰的对象。

self.__class__.__name__ 可以让你获取类的名字,但你也可以使用描述符以一种更通用的方式来实现这个功能。这个模式在一篇关于装饰器和描述符的博客文章中有详细描述,而你 logger 装饰器的具体实现可能看起来像这样:

class logger(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, type=None):
        return self.__class__(self.func.__get__(obj, type))
    def __call__(self, *args, **kw):
        print 'Entering %s' % self.func
        return self.func(*args, **kw)

class C(object):
    @logger
    def f(self, x, y):
        return x+y

C().f(1, 2)
# => Entering <bound method C.f of <__main__.C object at 0x...>>

显然,输出可以改进(例如,可以使用 getattr(self.func, 'im_class', None)),但这个通用模式对方法和函数都适用。不过,它对旧式类是不适用的(不过最好不要使用那些旧式类;)

51

Claudiu的回答是对的,但你也可以通过获取self参数的类名来“作弊”。这样做在继承的情况下可能会给出误导性的日志信息,但它能告诉你正在调用哪个对象的方法。例如:

from functools import wraps  # use this to preserve function signatures and docstrings
def logger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__)
        return func(*args, **kwargs)
    return with_logging

class C(object):
    @logger
    def f(self):
        pass

C().f()

正如我所说的,这种方法在你从父类继承了一个函数的情况下不会正常工作;在这种情况下,你可能会看到

class B(C):
    pass

b = B()
b.f()

并得到消息Entering B.f,而你实际上想要看到的是Entering C.f,因为那才是正确的类。不过,这种情况也可能是可以接受的,如果是这样的话,我会推荐这种方法,而不是Claudiu的建议。

撰写回答