Python 装饰器让函数忘记其所属类
我正在尝试写一个装饰器来进行日志记录:
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 个回答
这里提出的想法都很不错,但也有一些缺点:
inspect.getouterframes
和args[0].__class__.__name__
不适用于普通函数和静态方法。__get__
必须在一个类里面,这被@wraps
拒绝了。@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__()
函数在运行时才会变成方法。也就是说,当你调用 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)
),但这个通用模式对方法和函数都适用。不过,它对旧式类是不适用的(不过最好不要使用那些旧式类;)
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的建议。