关于反射:python decorator使函数忘记它属于一个类

Python decorator makes function forget that it belongs to a class

我正试图写一个装饰器来做日志记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
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()

我想打印:

1
Entering C.f

但是,我得到了这个错误消息:

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

大概这和"logger"中"myfunc"的范围有关,但我不知道是什么。


克劳迪乌的回答是正确的,但是你也可以通过从self参数中去掉类名来作弊。这会在继承的情况下给出误导性的日志语句,但会告诉您正在调用其方法的对象的类。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

正如我所说,如果您从父类继承了一个函数,那么这将无法正常工作;在这种情况下,您可能会说

1
2
3
4
5
class B(C):
    pass

b = B()
b.f()

得到消息Entering B.f,你实际上想要得到消息Entering C.f,因为这是正确的类。另一方面,这可能是可以接受的,在这种情况下,我建议采用这种方法,而不是克劳迪乌的建议。


函数只在运行时成为方法。也就是说,当你得到C.f时,你得到了一个绑定函数(和C.f.im_class is C)。在定义函数时,它只是一个普通函数,不绑定到任何类。这个未绑定和未关联的函数是由记录器修饰的。

self.__class__.__name__将为您提供类的名称,但您也可以使用描述符以更一般的方式完成这一任务。这个模式在一篇关于装饰器和描述符的博客文章中进行了描述,特别是日志装饰器的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)),但这种通用模式对方法和函数都有效。但是,它不适用于旧式类(但不要使用那些;)


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

  • inspect.getouterframesargs[0].__class__.__name__不适用于普通函数和静态方法。
  • __get__必须属于被@wraps拒绝的类别。
  • @wraps本身应该更好地隐藏痕迹。
  • 所以,我结合了这个页面的一些想法,链接,文档和我自己的头,最后找到了一个解决方案,它没有上述三个缺点。

    因此,method_decorator

    • 知道修饰方法绑定到的类。
    • 通过比functools.wraps()更正确地回答系统属性隐藏装饰器跟踪。
    • 包含绑定未绑定实例方法、类方法、静态方法和普通函数的单元测试。

    用途:

    1
    2
    3
    4
    5
    pip install method_decorator
    from method_decorator import method_decorator

    class my_decorator(method_decorator):
        # ...

    有关用法的详细信息,请参阅完整的单元测试。

    下面是method_decorator类的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    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__()


    似乎在创建类的同时,python创建常规的函数对象。之后它们只会变成未绑定的方法对象。知道了这一点,这是我唯一能找到做你想做的事情的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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函数,然后可以这样使用:

    1
    C = wrapClass(C)


    我使用inspect库找到了另一个解决类似问题的方法。当调用decorator时,即使函数尚未绑定到类,也可以检查堆栈并发现哪个类正在调用decorator。您至少可以获取类的字符串名称,如果这是您所需要的(可能还不能引用它,因为它正在被创建)。那么在创建类之后就不需要调用任何东西了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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()

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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__获取类的名称。


    如asaayers的答案所示,您不需要访问类对象。可能值得注意的是,由于python 3.3,您还可以使用__qualname__,它为您提供了完全限定的名称:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    >>> 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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    >>> 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'

    另外请注意,在python3中,im_class属性已经不存在了,因此,如果您真的希望访问decorator中的类,则需要另一个方法。我目前使用的方法涉及object.__set_name__,在我对"实例方法的python修饰符可以访问类"的回答中有详细说明。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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

    现在我们可以在访问时(__get__)和调用时(__call__)检查类。此机制适用于普通方法和静态类方法:

    1
    2
    >>> 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


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