2

I understand the @staticmethod decorator in practice. But a bug in mocking a static method led me down the Python semantics rabbit hole. This description in The standard type hierarchy section is confusing me:

Static method objects provide a way of defeating the transformation of function objects to method objects described above. A static method object is a wrapper around any other object, usually a user-defined method object. When a static method object is retrieved from a class or a class instance, the object actually returned is the wrapped object, which is not subject to any further transformation. Static method objects are not themselves callable, although the objects they wrap usually are. Static method objects are created by the built-in staticmethod() constructor.

The staticmethod() constructor takes a function object as sole argument. How can it wrap any other object than a function object? Even if this doesn't fail, how does it make any sense?

How is it usually a wrapper around a user-defined method object instead of a function object? User-defined method objects, when called, add the object they're called on to the start of the argument list, then call the function object stored on the class (ignoring all the various special cases).

How is it that static method objects are not themselves callable? How do calls to these work, then?

martineau
  • 119,623
  • 25
  • 170
  • 301
Ron HD
  • 161
  • 9
  • 1
    It could theoretically wrap any object. The key idea you are missing is that of a descriptor. It is how Python function objects do the instance binding in the first place. It isn't magic. You can implement it yourself. Read the following: docs.python.org/2.7/howto/descriptor.html So, the staticmethod object is a descriptor, and it's `__get__` method simply returns the object (pretty much always a function, but it doesn't have to be) itself, and that object is callable. But really, read that whole HOWTO, it should be very illuminating. – juanpa.arrivillaga Jan 08 '20 at 22:15
  • See also: https://stackoverflow.com/questions/55317094/are-staticmethod-and-classmethod-in-python-not-callable but there the answer is specifically about classmethod – juanpa.arrivillaga Jan 08 '20 at 22:16
  • @juanpa.arrivillaga I actually did read that descriptor doc, but it left too many unanswered questions. I'm still too new to Python to really grasp some of its more esoteric aspects (I only got into it due to a bug in mock's handling of static methods, fixed in a later version of the module than what I'm stuck with). That SO question didn't really help, but one it links to, https://stackoverflow.com/questions/38364980/why-are-python-static-class-method-not-callable, did. – Ron HD Jan 10 '20 at 00:05

2 Answers2

3

You can see that staticmethod can take any argument:

>>> x = staticmethod(3)

and that it is, indeed, not callable:

>>> x()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'staticmethod' object is not callable

staticmethod doesn't do much more than store a reference to its argument. The "magic" happens when you try to access a staticmethod object as the attribute of a class object or an instance of a class. When you do, you get the result of the staticmethod method's __get__ method, which is... the thing you originally wrapped.

>>> x.__get__(x)
3

Don't worry about why we passed x as an argument; suffice it to say, staticmethod.__get__ mostly ignores its argument(s).

When you wrap a function in a class statement, the staticmethod saves a reference to the function, to be called later when you ask for it.

>>> class Foo(object):
...   @staticmethod
...   def x():
...     pass
...
>>> type(Foo.__dict__['x'])
<type 'staticmethod'>
>>> type(Foo.x)
<type 'function'>

Instance methods work the way they do because function.__get__ returns an instance of method, which is in some sense just the original function partially applied the instance that invokes it. You may have seen that x.foo() is the same as type(x).foo(x). The reason that is true is because x.foo first resolves to type(x).foo, which itself evaluates to type(x).__dict__['foo'].__get__(x, type(x). The return value of function.__get__ is basically a wrapper around the function foo, but with x already supplied as the first argument.

staticmethod's main purpose is to provide a different __get__ method.

Incidentally, classmethod serves the same purpose. classmethod.__get__ returns something that calls the wrapped function with the class as the first argument, whether you invoke the class method from an instance of the class or the class itself.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I can only accept one answer, but I like yours too. The detail about Foo.__dict__['x'] being different than Foo.x is interesting. Of course, now I'm wondering why user-defined method objects don't just work the same way as static method objects... – Ron HD Jan 10 '20 at 00:13
  • Whether or not `x` is a static method or a regular instance method, `foo.x` is equivalent to `type(foo).__dict__['x'].__get__(foo, type(foo)). The difference, then, is what that `__get__` method returns. If `x` was decorated with `@staticmethod`, you just get back the original function `x`. If `x` was undecorated, you get back a `method` instance, which if you call, just passes `foo` and `x`'s own arguments to the original function. – chepner Jan 10 '20 at 01:30
  • But then why is the method object callable if it's __call__ method is not used, only its __get__ method? – Ron HD Jan 10 '20 at 20:30
  • `x` is a `function` object, not a `method` object (though we informally call `x` a method). The result of `x.__get__` is the `method` object. – chepner Jan 10 '20 at 20:34
2

How can it wrap any other object than a function object?

Pretty easily.

class Example(object):
    example = staticmethod(5)

print(Example.example) # prints 5

You can pass anything you want to the staticmethod constructor.

Even if this doesn't fail, how does it make any sense?

It usually doesn't, but staticmethod doesn't check.

How is it usually a wrapper around a user-defined method object instead of a function object?

It's not. That part's just wrong.

How is it that static method objects are not themselves callable? How do calls to these work, then?

The descriptor protocol. Static method objects have a __get__ method that returns whatever object they wrap. Attribute access invokes this __get__ method and returns what __get__ returns.

user2357112
  • 260,549
  • 28
  • 431
  • 505