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
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}