[ACCEPTED]-Python decorator makes function forget that it belongs to a class-metaprogramming
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.
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 ;)
Ideas proposed here are excellent, but have 6 some disadvantages:
inspect.getouterframes
andargs[0].__class__.__name__
are not suitable for plain functions and static-methods.__get__
must be in a class, that is rejected by@wraps
.@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__()
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)
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.
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.
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?"
You can also use new.instancemethod()
to create an instance 2 method (either bound or unbound) from a 1 function.
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
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.