Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check if a function uses @classmethod

TL;DR How do I find out whether a function was defined using @classmethod or something with the same effect?


My problem

For implementing a class decorator I would like to check if a method takes the class as its first argument, for example as achieved via

@classmethod
def function(cls, ...):

I found a solution to check for @staticmethod via the types module (isinstance(foo, types.UnboundMethodType) is False if the foo is static, see here), but did not find anything on how to do so for @classmethod


Context

What I am trying to do is something along the lines of

def class_decorator(cls):
    for member in cls.__dict__:
        if (isclassmethod(getattr(cls, member))):
            # do something with the method
            setattr(cls, member, modified_method)
    return cls

and I do not know how to implement what I called isclassmethod in this example

like image 892
hlt Avatar asked Oct 07 '13 14:10

hlt


People also ask

Do I need to use @classmethod in Python?

MethodName() . The class method can also be called using an object of the class. The @classmethod is an alternative of the classmethod() function. It is recommended to use the @classmethod decorator instead of the function because it is just a syntactic sugar.

What is @classmethod used for?

The @classmethod decorator is a built-in function decorator which is an expression that gets evaluated after your function is defined. The result of that evaluation shadows your function definition. A class method receives the class as the implicit first argument, just like an instance method receives the instance.

What is the difference between @staticmethod and @classmethod in Python?

The static method does not take any specific parameter. Class method can access and modify the class state. Static Method cannot access or modify the class state. The class method takes the class as parameter to know about the state of that class.

When should you use a Classmethod Python?

You can use class methods for any methods that are not bound to a specific instance but the class. In practice, you often use class methods for methods that create an instance of the class. When a method creates an instance of the class and returns it, the method is called a factory method.


2 Answers

If the object is a method object, and so has a method.__self__ attribute, and that attribute is the class you got the attribute from, then it'll take the class as the first argument. It has been bound to the class.

Note that you already have a bound object at this point, so you don't need to pass in the class again, unless you first extract the original function from method.__func__.

Here is an illustration, the class Foo has a class method bar and a regular method baz, which is not bound when you access it directly on the class:

>>> class Foo:
...     @classmethod
...     def bar(cls):
...         pass
...     def baz(self):
...         pass
... 
>>> Foo.baz
<function Foo.baz at 0x1097d1e18>
>>> Foo.bar
<bound method Foo.bar of <class '__main__.Foo'>>
>>> Foo.bar.__self__
<class '__main__.Foo'>
>>> Foo.bar.__self__ is Foo
True

Calling Foo.bar() automatically passes in Foo.bar.__self__ as the first argument.

If you need to test such methods, use inspect.ismethod(), and if that returns True test the __self__ attribute:

import inspect

if inspect.ismethod(cls.method) and cls.method.__self__ is cls:
    # method bound to the class, e.g. a classmethod

This should work for any custom descriptors that work like classmethod does, as well.

If you need to know with certainty that the method was produced by a classmethod object, you'll need to look up the attributes directly in the class namespace (cls.__dict__ or vars(cls)), and do so in each class in the class hierarchy in method resolution order:

def isclassmethod(method):
    bound_to = getattr(method, '__self__', None)
    if not isinstance(bound_to, type):
        # must be bound to a class
        return False
    name = method.__name__
    for cls in bound_to.__mro__:
        descriptor = vars(cls).get(name)
        if descriptor is not None:
            return isinstance(descriptor, classmethod)
    return False

and a full test of the above two approaches using a base class and a derived class, with a custom descriptor that binds a function the same way a classmethod would, but is not, itself, a classmethod:

>>> class notclassmethod:
...     def __init__(self, f):
...         self.f = f
...     def __get__(self, _, typ=None):
...         return self.f.__get__(typ, typ)
...
>>> class Base:
...     @classmethod
...     def base_cm(cls): pass
...     @notclassmethod
...     def base_ncm(cls): pass
...     def base_m(self): pass
...
>>> class Derived(Base):
...     @classmethod
...     def derived_cm(cls): pass
...     @notclassmethod
...     def derived_ncm(cls): pass
...     def derived_m(self): pass
...
>>> inspect.ismethod(Derived.base_cm) and Derived.base_cm.__self__ is Derived
True
>>> inspect.ismethod(Derived.base_ncm) and Derived.base_ncm.__self__ is Derived
True
>>> inspect.ismethod(Derived.base_m) and Derived.base_m.__self__ is Derived
False
>>> inspect.ismethod(Derived.derived_cm) and Derived.derived_cm.__self__ is Derived
True
>>> inspect.ismethod(Derived.derived_ncm) and Derived.derived_ncm.__self__ is Derived
True
>>> inspect.ismethod(Derived.derived_m) and Derived.derived_m.__self__ is Derived
False
>>> isclassmethod(Derived.base_cm)
True
>>> isclassmethod(Derived.base_ncm)
False
>>> isclassmethod(Derived.base_m)
False
>>> isclassmethod(Derived.derived_cm)
True
>>> isclassmethod(Derived.derived_ncm)
False
>>> isclassmethod(Derived.derived_m)
False

The isclassmethod() function correctly distinguishes between the classmethod and notclassmethod descriptors.


Historical note: this answer included references to Python 2, but with Python 2 having reached EOL were removed as no longer relevant.

like image 143
Martijn Pieters Avatar answered Nov 06 '22 02:11

Martijn Pieters


You should use inspect.ismethod. It works because classmethod binds the function to the class object. See the following code:

>>> class Foo:
...     @classmethod
...     def bar():
...             pass
...     def baz():
...             pass
...
>>> Foo.bar
<bound method type.bar of <class '__main__.Foo'>>
>>> Foo.baz
<function Foo.baz at 0x0000000002CCC1E0>
>>> type(Foo.bar)
<class 'method'>
>>> type(Foo.baz)
<class 'function'>
>>> import inspect
>>> inspect.ismethod(Foo.bar)
True
>>> inspect.ismethod(Foo.baz)
False
like image 36
Simon Bergot Avatar answered Nov 06 '22 02:11

Simon Bergot