pythondecorator

inspect.signature on class methods


Can someone help me understand why the Signature.bind(...).arguments are different when I call directly for a class method vs. when used in a decorator?

I have the decorator below.

def decorator(func):     
    @functools.wraps(func)
    def inner(*args, **kwargs): 
        print(inspect.signature(func).bind(*args, **kwargs).arguments)
        return func(*args, **kwargs)
    return inner

When this decorator runs on a class method, I get the cls parameter in the signature like below.

class MyClass: 

    @classmethod
    @decorator
    def mymethod(cls, a, b): 
        return a + b

MyClass.mymethod(1, 2)

# returns: 
# {'cls': <class '__main__.MyClass'>, 'a': 1, 'b': 2}
# 3

If I call inspect.signature on the class method directly, I get a different result or an error.

This first block runs successfully but does not return the class parameter.

print(inspect.signature(MyClass.mymethod).bind(1, 2).arguments)
# returns {'a': 1, 'b': 2}

This errors out.

print(inspect.signature(MyClass.mymethod).bind(MyClass, 1, 2).arguments)
# TypeError: too many positional arguments

Solution

  • The classmethod decorator changes the signature of a function. When classmethod is applied to a function that is stored on a class, it automatically fills the first parameter of the function as the class. This is much the same as how self parameter is filled as the object a method is called on.

    If you print out the two signatures you can see this ie.

    >>> inspect.signature(MyClass.mymethod) # bound method, cls need not be supplied manually
    <Signature (a, b)>
    >>> inspect.signature(MyClass.mymethod.__func__) # gets unbound, underlying function
    <Signature (cls, a, b)>
    

    That is, your decorator is working with a pure function. This is because your decorator is applied before the classmethod decorator. Decorators are applied bottom to top (in the order they appear closest to the function def). ie.

    @applied_third
    @applied_second
    @applied_first
    def foo(): pass
    

    Your decorator must be applied first. This is due to how classmethods work, and something called non-data descriptors. As such, you MUST supply the cls argument as it was supplied to it. When you do MyClass.mymethod, you get a bound method, whose first parameter has been bound to MyClass. As such you MUST NOT supply the cls parameter manually (as it is has already been done automatically for you).

    Try the following instead:

    >>> inspect.signature(MyClass.mymethod).bind(1, 2)
    {'a': 1, 'b': 2}