Can a decorator of an instance method access the class?
Solution 1
If you are using Python 2.6 or later you could use a class decorator, perhaps something like this (warning: untested code).
def class_decorator(cls):
for name, method in cls.__dict__.iteritems():
if hasattr(method, "use_class"):
# do something with the method and class
print name, cls
return cls
def method_decorator(view):
# mark the method as something that requires view's class
view.use_class = True
return view
@class_decorator
class ModelA(object):
@method_decorator
def a_method(self):
# do some stuff
pass
The method decorator marks the method as one that is of interest by adding a "use_class" attribute - functions and methods are also objects, so you can attach additional metadata to them.
After the class has been created the class decorator then goes through all the methods and does whatever is needed on the methods that have been marked.
If you want all the methods to be affected then you could leave out the method decorator and just use the class decorator.
Solution 2
Since python 3.6 you can use object.__set_name__
to accomplish this in a very simple way. The doc states that __set_name__
is "called at the time the owning class owner is created".
Here is an example:
class class_decorator:
def __init__(self, fn):
self.fn = fn
def __set_name__(self, owner, name):
# do something with owner, i.e.
print(f"decorating {self.fn} and using {owner}")
self.fn.class_name = owner.__name__
# then replace ourself with the original method
setattr(owner, name, self.fn)
Notice that it gets called at class creation time:
>>> class A:
... @class_decorator
... def hello(self, x=42):
... return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42
If you want to know more about how classes are created and in particular exactly when __set_name__
is called, you can refer to the documentation on "Creating the class object".
Solution 3
As others have pointed out, the class hasn't been created at the time the decorator is called. However, it's possible to annotate the function object with the decorator parameters, then re-decorate the function in the metaclass's __new__
method. You'll need to access the function's __dict__
attribute directly, as at least for me, func.foo = 1
resulted in an AttributeError.
Solution 4
As Mark suggests:
- Any decorator is called BEFORE class is built, so is unknown to the decorator.
- We can tag these methods and make any necessary post-process later.
- We have two options for post-processing: automatically at the end of the class definition or somewhere before the application will run. I prefer the 1st option using a base class, but you can follow the 2nd approach as well.
This code shows how this may works using automatic post-processing:
def expose(**kw):
"Note that using **kw you can tag the function with any parameters"
def wrap(func):
name = func.func_name
assert not name.startswith('_'), "Only public methods can be exposed"
meta = func.__meta__ = kw
meta['exposed'] = True
return func
return wrap
class Exposable(object):
"Base class to expose instance methods"
_exposable_ = None # Not necessary, just for pylint
class __metaclass__(type):
def __new__(cls, name, bases, state):
methods = state['_exposed_'] = dict()
# inherit bases exposed methods
for base in bases:
methods.update(getattr(base, '_exposed_', {}))
for name, member in state.items():
meta = getattr(member, '__meta__', None)
if meta is not None:
print "Found", name, meta
methods[name] = member
return type.__new__(cls, name, bases, state)
class Foo(Exposable):
@expose(any='parameter will go', inside='__meta__ func attribute')
def foo(self):
pass
class Bar(Exposable):
@expose(hide=True, help='the great bar function')
def bar(self):
pass
class Buzz(Bar):
@expose(hello=False, msg='overriding bar function')
def bar(self):
pass
class Fizz(Foo):
@expose(msg='adding a bar function')
def bar(self):
pass
print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)
print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)
The output yields:
Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}
Note that in this example:
- We can annotate any function with any arbitrary parameters.
- Each class has its own exposed methods.
- We can inherit exposed methods as well.
- methods can be overriding as exposing feature is updated.
Hope this helps
Solution 5
As Ants indicated, you can't get a reference to the class from within the class. However, if you're interested in distinguishing between different classes ( not manipulating the actual class type object), you can pass a string for each class. You can also pass whatever other parameters you like to the decorator using class-style decorators.
class Decorator(object):
def __init__(self,decoratee_enclosing_class):
self.decoratee_enclosing_class = decoratee_enclosing_class
def __call__(self,original_func):
def new_function(*args,**kwargs):
print 'decorating function in ',self.decoratee_enclosing_class
original_func(*args,**kwargs)
return new_function
class Bar(object):
@Decorator('Bar')
def foo(self):
print 'in foo'
class Baz(object):
@Decorator('Baz')
def foo(self):
print 'in foo'
print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()
Prints:
before instantiating Bar()
calling b.foo()
decorating function in Bar
in foo
Carl G
Full-stack developer. Currently working in big data analytics. #SOreadytohelp
Updated on August 02, 2022Comments
-
Carl G almost 2 years
I have something roughly like the following. Basically I need to access the class of an instance method from a decorator used upon the instance method in its definition.
def decorator(view): # do something that requires view's class print view.im_class return view class ModelA(object): @decorator def a_method(self): # do some stuff pass
The code as-is gives:
AttributeError: 'function' object has no attribute 'im_class'
I found similar question/answers - Python decorator makes function forget that it belongs to a class and Get class in Python decorator - but these rely upon a workaround that grabs the instance at run-time by snatching the first parameter. In my case, I will be calling the method based upon the information gleaned from its class, so I can't wait for a call to come in.
-
Carl G over 14 yearsThanks for confirming my depressing conclusion that this isn't possible. I could also use a string that fully qualified the module/class ('module.Class'), store the string(s) until the classes have all fully loaded, then retrieve the classes myself with import. That seems like a woefully un-DRY way to accomplish my task.
-
Carl G over 14 yearsThanks but this is exactly the solution I referenced in my question that doesn't work for me. I am trying to implement an observer pattern using decorators and I will never be able to call the method in the correct context from my observation dispatcher if I don't have the class at some point while adding the method to the observation dispatcher. Getting the class upon method call doesn't help me correctly call the method in the first place.
-
Carl G over 14 yearswhen one defines a function the function doesn't exist yet, but one is able to recursively call the function from within itself. I guess this is a language feature specific to functions and not available to classes.
-
Will McCutchen over 14 yearsWhoa, sorry for my laziness in not reading your entire question.
-
u0b34a0f6ae over 14 yearsDGGenuine: The function is only called, and the function thus accesses itself, only after it was created completely. In this case, the class can not be complete when the decorator is called, since the class must wait for the decorator's result, which will be stored as one of the attributes of the class.
-
Carl G over 14 yearsThanks I think this is the route with which to go. Just one extra line of code for any class I'd want to use this decorator. Maybe I could use a custom metaclass and perform this same check during new...?
-
Carl G about 14 yearsAnyone trying to use this with staticmethod or classmethod will want to read this PEP: python.org/dev/peps/pep-0232 Not sure it's possible because you can't set an attribute on a class/static method and I think they gobble up any custom function attributes when they are applied to a function.
-
Erik Kaplun almost 12 yearsYou don't need to use a class for this sort of decorator: the idiomatic approach is to use one extra level of nested functions inside the decorator function. However, if you do go with classes, it might be nicer to not use capitalisation in the class name to make the decoration itself look "standard", i.e.
@decorator('Bar')
as opposed to@Decorator('Bar')
. -
Coyote21 almost 12 yearsJust what I was looking for, for my DBM based ORM... Thanks, dude.
-
schlamar over 11 yearsYou should use
inspect.getmro(cls)
to process all base classes in the class decorator to support inheritance. -
schlamar over 11 years
setattr
should be used instead of accessing__dict__
-
Anentropic about 10 years@schlamar do you mean instead of
cls.__dict__
? to first get the mro classes, then iterate over them and docls.__dict__
for each? -
Anentropic about 10 yearsoh, actually it looks like
inspect
to the rescue stackoverflow.com/a/1911287/202168 -
Anentropic about 10 yearsthat's a useful pattern, but this doesn't address the problem of a method decorator being able to refer to the parent class of the method it's applied to
-
charlax about 10 yearsI updated my answer to be more explicit how this can be useful to get access to the class at import time (i.e. using a metaclass + caching the decorator param on the method).
-
Cecil Curry almost 6 yearsSadly, this approach is functionally equivalent to Will McCutchen's equally inapplicable answer. Both this and that answer obtain the desired class at method call time rather than method decoration time, as required by the original question. The only reasonable means of obtaining this class at a sufficiently early time is to introspect over all methods at class definition time (e.g., via a class decorator or metaclass).
</sigh>
-
luckydonald over 4 yearsHow would that look like for using the decorator with parameters? E.g.
@class_decorator('test', foo='bar')
-
luckydonald over 4 yearsHow can I make this method decorator accept values? I'm thinking
@method_decorator('foobar')
should result inview.use_class = 'foobar'
. -
Matt Eding over 4 years@luckydonald You can approach it similar to normal decorators that take arguments. Just have
def decorator(*args, **kwds): class Descriptor: ...; return Descriptor
-
kawing-chiu over 4 yearsWow, thank you very much. Didn't know about
__set_name__
although I've been using Python 3.6+ for a long time. -
kawing-chiu over 4 yearsThere is one drawback of this method: the static checker does not understand this at all. Mypy will think that
hello
is not a method, but instead is an object of typeclass_decorator
. -
tyrion over 4 years@kawing-chiu If nothing else works, you can use an
if TYPE_CHECKING
to defineclass_decorator
as a normal decorator returning the correct type. -
Mark Gerolimatos about 4 yearsPLEASE NOTE: in Python 3, if you are willing to put up with string parsing, the function passed in to a decorator will contain the
__qualname__
dundermember, which will be<Class>.<function name>
I presume that it will be a class PATH if you have embedded classes. If the function is global, you just get the function name, no module name. -
EvilTosha about 4 yearsYou have a typo in this line:
if inspect.isfunction(v) and getattr(k, "is_field", False):
it should begetattr(v, "is_field", False)
instead. -
Pol almost 4 yearsI think this is not reliable, because decorator might be stacked in combination with other decorators. So if it is wrapped by another decorator than it might not be called.
-
steezeburger over 3 years@Pol thank you so much for that comment. I was trying to debug why this was not working for me. Do you know what the solution would be to use this with the
@classmethod
decorator as well? -
Paul Whipp about 3 yearsIn Python 3 the method object has the self attribute. Calling type on that will return the class in which the method is defined.