Python 装饰器使函数忘记它属于一个类

Python decorator makes function forget that it belongs to a class

提问人:Charles Anderson 提问时间:11/21/2008 最后编辑:xianCharles Anderson 更新时间:1/23/2019 访问量:28227

问:

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

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”范围有关,但我不知道是什么。

Python 反射 元编程

评论

0赞 apcelent 12/18/2015
不完全是答案,但发现这篇文章涵盖了深入 bit.ly/1NsBLmx

答:

7赞 Claudiu 11/21/2008 #1

似乎在创建类时,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):
    def f(self):
        pass
C.f = logger(C.f)
C().f()

这将输出所需的结果。

如果你想将所有方法包装在一个类中,那么你可能想要创建一个 wrapClass 函数,然后你可以像这样使用它:

C = wrapClass(C)

评论

0赞 Piotr Lesnicki 11/21/2008
由于 static 方法,wrapclass 应该小心。
0赞 babbageclunk 11/21/2008
这看起来像是类装饰器(Python 2.6 中的新功能)的一个很好的用例。它们的工作方式与函数装饰器完全相同。
6赞 Asa Ayers 11/21/2008 #2

类函数应始终将 self 作为其第一个参数,因此您可以使用它而不是 im_class。

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

    return new 

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

起初我想使用,但这不起作用,因为实例没有名称。必须使用才能获取类的名称。self.__name__self.__class__.__name__

51赞 Eli Courtwright 11/21/2008 #3

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()

并在您真正想要获取消息的地方获取消息,因为这是正确的类。另一方面,这可能是可以接受的,在这种情况下,我会推荐这种方法而不是 Claudiu 的建议。Entering B.fEntering C.f

评论

1赞 Piotr Lesnicki 11/21/2008
错别字:你忘了记录器功能。return with_logging
2赞 Piotr Lesnicki 11/21/2008
顺便说一句,functools.wraps 不保留 im_* 属性。你认为这个省略可以被认为是一个错误吗?
1赞 Charles Anderson 11/21/2008
我还不能假装我完全理解@wraps是怎么回事,但它确实解决了我的问题。非常感谢。
0赞 Eli Courtwright 11/21/2008
Piotr:感谢您指出缺失的回报;我编辑了我的帖子来解决这个问题。至于 im_* 属性,在说这绝对是一个错误之前,我必须考虑复制这些属性的所有含义。但是,我想不出省略它们的好理由。
1赞 Eli Courtwright 11/21/2008
Charles:我在 Stack Overflow 上发布了另一个问题,解释了包装的使用:stackoverflow.com/questions/308999/what-does-functoolswraps-do
0赞 Andrew Beyer 11/21/2008 #4

还可以用于从函数创建实例方法(绑定或未绑定)。new.instancemethod()

30赞 ianb 11/21/2008 #5

函数仅在运行时成为方法。也就是说,当你得到时,你会得到一个绑定函数(和)。在定义函数时,它只是一个普通函数,它不绑定到任何类。这个未绑定和分离的函数是 logger 修饰的。C.fC.f.im_class is C

self.__class__.__name__将为您提供类的名称,但您也可以使用描述符以更通用的方式完成此操作。此模式在有关装饰器和描述符的博客文章中进行了描述,特别是记录器装饰器的实现如下所示:

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)

评论

0赞 theannouncer 6/8/2017
对于其他任何寻找函数给出或意识到参数 s 未传递给您修饰的函数的人,这就是解决方案,谢谢@ianbTypeError: foo takes exactly x argumentsattribute missingself
0赞 KeatsKelleher 1/5/2018
此方法需要不同的记录器来记录绑定方法、未绑定方法和函数日志记录。
0赞 Jan Rüegg 2/10/2020
@KeatsKelleher怎么会这样?在我的测试中,装饰和调用函数似乎工作正常......
7赞 user398139 7/21/2010 #6

我使用该库找到了另一种解决方案来解决一个非常相似的问题。调用修饰器时,即使函数尚未绑定到类,也可以检查堆栈并发现哪个类正在调用修饰器。如果这就是您所需要的,您至少可以获取类的字符串名称(可能还不能引用它,因为它正在创建中)。然后,在创建类后,无需调用任何内容。inspect

import inspect

def logger(myFunc):
    classname = inspect.getouterframes(inspect.currentframe())[1][3]
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (classname, myFunc.__name__)
        return myFunc(*args, **keyargs)
    return new

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

C().f()

虽然这不一定比其他方法更好,但这是我在调用装饰器期间发现未来方法的类名的唯一方法。请注意不要在库文档中保留对框架的引用。inspect

评论

2赞 203 7/25/2015
这正是我想要的 - 在第一次调用之前,有关方法和它将绑定到的类的信息。
18赞 Denis Ryzhkov 8/5/2010 #7

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

  1. inspect.getouterframes并且不适用于普通函数和静态方法。args[0].__class__.__name__
  2. __get__必须位于被 拒绝的类中。@wraps
  3. @wraps本身应该更好地隐藏痕迹。

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

因此,:method_decorator

  • 知道修饰方法绑定到的类。
  • 通过比 Proper 更正确地回答系统属性来隐藏修饰器跟踪。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__()

评论

0赞 Simon 9/4/2014
这是唯一对我有用的方法。我需要对象实例引用
0赞 aurzenligl 1/29/2018 #8

当函数不知道它是类时,不要在定义时注入装饰代码,而是延迟运行此代码,直到访问/调用函数。描述符对象有助于在访问/调用时延迟注入自己的代码:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        return self.__class__(self.func.__get__(obj, type_), type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

现在,我们可以在访问时间()和调用时间()检查类。此机制适用于普通方法以及 static|class 方法:__get____call__

>>> Foo().foo(1, b=2)
called Foo.foo with args=(1,) kwargs={'b': 2}

完整示例见:https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

3赞 tyrion 1/23/2019 #9

Asa Ayers 的回答所示,您不需要访问类对象。值得一提的是,从 Python 3.3 开始,您还可以使用 __qualname__,它为您提供了完全限定的名称:

>>> def logger(myFunc):
...     def new(*args, **keyargs):
...         print('Entering %s' % myFunc.__qualname__)
...         return myFunc(*args, **keyargs)
... 
...     return new
... 
>>> class C(object):
...     @logger
...     def f(self):
...         pass
... 
>>> C().f()
Entering C.f

这还有一个额外的优点,即在嵌套类的情况下也有效,如 PEP 3155 中的以下示例所示:

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

另请注意,在 Python 3 中,该属性已消失,因此,如果您真的希望在装饰器中访问该类,则需要其他方法。我目前使用的方法涉及object.__set_name__并在我对“实例方法的 Python 装饰器能否访问该类”的回答中进行了详细说明。im_class