代码之家  ›  专栏  ›  技术社区  ›  Charles Anderson

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

  •  64
  • Charles Anderson  · 技术社区  · 17 年前

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

    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 回复  |  直到 16 年前
        1
  •  43
  •   Eli Courtwright    17 年前

    克劳迪的答案是正确的,但你也可以通过去掉类名来作弊 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 因为那是正确的班级。另一方面,这可能是可以接受的,在这种情况下,我会推荐这种方法,而不是克劳迪的建议。

        2
  •  25
  •   Community Mohan Dere    15 年前

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

    self.__class__.__name__ 将为您提供类的名称,但您也可以使用描述符以更通用的方式完成此操作。此模式已描述 in a blog post on Decorators and Descriptors ,你的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) ),但这种通用模式适用于方法和函数。然而,它将 不 为旧式课程工作(但不要使用这些课程;)

        3
  •  16
  •   Denis Ryzhkov    13 年前

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

    1. inspect.getouterframes 和 args[0].__class__.__name__ 不适合普通函数和静态方法。
    2. __get__ 必须在被拒绝的班级中 @wraps .
    3. @包裹 它本身应该能更好地隐藏痕迹。

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

    因此, method_decorator :

    • 知道装饰方法所属的类。
    • 通过更正确地响应系统属性来隐藏装饰器痕迹 functools.wraps() 做。
    • 涵盖了绑定未绑定实例方法、类方法、静态方法和普通函数的单元测试。

    用途:

    pip install method_decorator
    from method_decorator import method_decorator
    
    class my_decorator(method_decorator):
        # ...
    

    看见 full unit-tests for usage details .

    这只是代码 方法论 类别:

    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__()
    
        4
  •  7
  •   Claudiu    17 年前

    似乎在创建类的同时,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)
    
        5
  •  6
  •   Asa Ayers    17 年前

    我使用 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()
    

    虽然这不一定 较好的 与其他人相比,它是 仅 我可以在调用装饰器时找出未来方法的类名。注意不要在文档中保留对框架的引用 检查 图书馆文献。

        6
  •  6
  •   user398139    16 年前

    类函数应始终将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__ 获取类的名称。

        7
  •  0
  •   Andrew Beyer    17 年前

    如…所示 Asa Ayers' answer ,您不需要访问类对象。值得知道的是,自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中 im_class 属性消失了,因此,如果你真的想在装饰器中访问该类,你需要一个其他方法。我目前使用的方法包括 object.__set_name__ 详见 my answer to "Can a Python decorator of an instance method access the class?"

        8
  •  0
  •   aurzenligl    8 年前

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

        9
  •  0
  •   tyrion    7 年前

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

    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__ ).此机制适用于普通方法以及静态|类方法:

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