Say I want to make a decorator for methods defined in a class. I want that decorator, when invoked, to be able to set an attribute on the class defining the method (in order to register it in a list of methods that serve a particular purpose).
In Python 2, the im_class
method accomplishes this nicely:
def decorator(method):
cls = method.im_class
cls.foo = 'bar'
return method
However, in Python 3, no such attribute (or a replacement for it) seems to exist. I suppose the idea was that you could call type(method.__self__)
to get the class, but this does not work for unbound methods, since __self__ == None
in that case.
NOTE: This question is actually a bit irrelevant for my case, since I've chosen instead to set an attribute on the method itself and then have the instance scan through all of its methods looking for that attribute at the appropriate time. I am also (currently) using Python 2.6. However, I am curious if there is any replacement for the version 2 functionality, and if not, what the rationale was for removing it completely.
EDIT: I just found this question. This makes it seem like the best solution is just to avoid it like I have. I'm still wondering why it was removed though.
The point you appear to be missing is, in Python 3 the "unbound method" type has entirely disappeared -- a method, until and unless it's bound, is just a function, without the weird "type-checking" unbound methods used to perform. This makes the language simpler!
To wit...:
and voila -- one less subtle concept and distinction to worry about. Such simplifications are the core advantage of Python 3 wrt Python 2, which (over the years) had been accumulating so many subtleties that it was in danger (if features kept being added to it) of really losing its status as a simple language. With Python 3, simplicity is back!-)
I thought it would be worthwhile writing something that does it best at guessing the defining class. For completeness' sake this answer also addresses bound methods.
At worst, guessing should fail altogether, with the function returning
None
. However, under any circumstances, it shouldn't return an incorrect class.TL;DR
The final version of our function successfully overcomes most simple cases, and a few pitfalls as well.
In a nutshell, its implementation differentiates between bound methods and “unbound methods“ (functions) since in
Python 3
there is no reliable way to extract the enclosing class from an “unbound method".MRO
, in a similar manner to that done in the accepted answer to an equivalent question forPython 2
.Python 3.3
and is quite reckless (if this feature is unnecessary it's probably best to remove this block of code and just returnNone
instead).There is also special handling for methods defined via descriptors, that aren't classified as ordinary methods or functions (for example,
set.union
,int.__add__
andint().__add__
).The resulting function is:
A small request
If you decide to use this implementation, and encounter any caveats, please comment and describe what happened.
“Unbound methods” are regular functions
First of all, it's worth noting the following change made in
Python 3
(see Guido's motivation here):This makes it practically impossible to reliably extract the class in which a certain “unbound method“ was defined unless it's bound to an object of that class (or of one of its subclasses).
Handling bound methods
So, let us first handle the “easier case“ in which we have a bound method. Note that the bound method must be written in
Python
, as described ininspect.ismethod
's documentation.However, this solution is not perfect and has its perils, as methods can be assigned in runtime, rendering their name possibly different than that of the attribute that they are assigned to (see example below). This problem exists also in
Python 2
. A possible workaround would be to iterate over all of the class's attributes, looking for one whose identity is that of the specified method.Handling “unbound methods“
Now that we got that out of the way, we can suggest a hack that tries to handle “unbound methods”. The hack, its rationale, and some discouragement words can be found in this answer. It relies on manually parsing the
__qualname__
attribute, available only fromPython 3.3
, is highly unrecommended, but should work for simple cases:Combining both approaches
Since
inspect.isfunction
andinspect.ismethod
are mutually exclusive, combining both approaches into a single solution gives us the following (with added logging facilities for the upcoming examples):Execution example
So far, so good, but...
Final touches
Z.y
can be partially fixed (to returnNone
) by verifying that the returned value is a class, before actually returning it.Z().z
can be fixed by falling back to parsing the function's__qualname__
attribute (the function can be extracted viameth.__func__
).The outcome generated by
Z.class_meth
andZ().class_meth
is incorrect because accessing a class method always returns a bound method, whose__self__
attribute is the class itself, rather than its object. Thus, further accessing the__class__
attribute on top of that__self__
attribute doesn't work as expected:This can be fixed by checking whether the method's
__self__
attribute returns an instance oftype
. However, this might be confusing when our function is invoked against methods of a metaclass, so we'll leave it as is for now.Here is the final version:
Surprisingly, this also fixes the outcome of
Z.class_meth
andZ().class_meth
which now correctly returnZ
. This is because the__func__
attribute of a class method returns a regular function whose__qualname__
attribute may be parsed:EDIT:
As per the issue raised by Bryce, it's possible to handle
method_descriptor
objects, likeset.union
, andwrapper_descriptor
objects, likeint.__add__
, merely by returning their__objclass__
attribute (introduced by PEP-252), if such exists:However,
inspect.ismethoddescriptor
returnsFalse
for the respective instance method objects, i.e. forset().union
and forint().__add__
:Since
int().__add__.__objclass__
returnsint
, the above if clause may be relinquished in order to solve the problem forint().__add__
.Unfortunately, this doesn't address the matter of
set().union
, for which no__objclass__
attribute is defined.