Search code examples
pythondecoratorclass-methodpython-decorators

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


Solution

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