[ACCEPTED]-Python decorator makes function forget that it belongs to a class-metaprogramming

Accepted answer
Score: 50

Claudiu's answer is correct, but you can 13 also cheat by getting the class name off 12 of the self argument. This will give misleading 11 log statements in cases of inheritance, but 10 will tell you the class of the object whose 9 method is being called. For example:

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

As 8 I said, this won't work properly in cases 7 where you've inherited a function from a 6 parent class; in this case you might say

class B(C):
    pass

b = B()
b.f()

and 5 get the message Entering B.f where you actually want 4 to get the message Entering C.f since that's the correct 3 class. On the other hand, this might be 2 acceptable, in which case I'd recommend 1 this approach over Claudiu's suggestion.

Score: 29

Functions only become methods at runtime. That 15 is, when you get C.f you get a bound function 14 (and C.f.im_class is C). At the time your function is defined 13 it is just a plain function, it is not bound 12 to any class. This unbound and disassociated 11 function is what is decorated by logger.

self.__class__.__name__ will 10 give you the name of the class, but you 9 can also use descriptors to accomplish this 8 in a somewhat more general way. This pattern 7 is described in a blog post on Decorators and Descriptors, and an implementation of 6 your logger decorator in particular would 5 look like:

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

Obviously the output can be improved 4 (by using, for example, getattr(self.func, 'im_class', None)), but this general 3 pattern will work for both methods and functions. However 2 it will not work for old-style classes (but 1 just don't use those ;)

Score: 18

Ideas proposed here are excellent, but have 6 some disadvantages:

  1. inspect.getouterframes and args[0].__class__.__name__ are not suitable for plain functions and static-methods.
  2. __get__ must be in a class, that is rejected by @wraps.
  3. @wraps itself should be hiding traces better.

So, I've combined some 5 ideas from this page, links, docs and my 4 own head,
and finally found a solution, that 3 lacks all three disadvantages above.

As a 2 result, method_decorator:

  • Knows the class the decorated method is bound to.
  • Hides decorator traces by answering to system attributes more correctly than functools.wraps() does.
  • Is covered with unit-tests for bound an unbound instance-methods, class-methods, static-methods, and plain functions.

Usage:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

See full unit-tests for usage details.

And here is just the 1 code of the method_decorator class:

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__()
Score: 7

It seems that while the class is being created, Python 8 creates regular function objects. They only 7 get turned into unbound method objects afterwards. Knowing 6 that, this is the only way I could find 5 to do what you want:

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

This outputs the desired 4 result.

If you want to wrap all the methods 3 in a class, then you probably want to create 2 a wrapClass function, which you could then 1 use like this:

C = wrapClass(C)
Score: 6

Class functions should always take self 5 as their first argument, so you can use 4 that instead of 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()

at first I wanted 3 to use self.__name__ but that doesn't work because the 2 instance has no name. you must use self.__class__.__name__ to get 1 the name of the class.

Score: 6

I found another solution to a very similar 15 problem using the inspect library. When the decorator 14 is called, even though the function is not 13 yet bound to the class, you can inspect 12 the stack and discover which class is calling 11 the decorator. You can at least get the 10 string name of the class, if that is all 9 you need (probably can't reference it yet 8 since it is being created). Then you do 7 not need to call anything after the class 6 has been created.

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

While this is not necessarily 5 better than the others, it is the only way I can figure 4 out to discover the class name of the future 3 method during the call to the decorator. Make 2 note of not keeping references to frames 1 around in the inspect library documentation.

Score: 3

As shown in Asa Ayers' answer, you don't need to access the 11 class object. It may be worth to know that 10 since Python 3.3, you can also use __qualname__, which 9 gives you the fully qualified name:

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

This 8 has the added advantage of working also 7 in the case of nested classes, as shown 6 in this example taken from 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'

Notice also 5 that in Python 3 the im_class attribute is gone, therefore 4 if you really wish to access the class in 3 a decorator, you need an other method. The 2 approach I currently use involves object.__set_name__ and is 1 detailed in my answer to "Can a Python decorator of an instance method access the class?"

Score: 0

You can also use new.instancemethod() to create an instance 2 method (either bound or unbound) from a 1 function.

Score: 0

Instead of injecting decorating code at 9 definition time, when function doesn't know 8 it's class, delay running this code until 7 function is accessed/called. Descriptor 6 object facilitates injecting own code late, at 5 access/call time:

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

Now we can inspect class 4 both at access time (__get__) and at call time 3 (__call__). This mechanism works for plain methods 2 as well as static|class methods:

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

Full example 1 at: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

More Related questions