bpo-43682: Make staticmethod objects callable by vstinner · Pull Request #25117 · python/cpython
@serhiy-storchaka @gvanrossum @rhettinger: so, what do you think? this idea was rejected in 2015, but came back in 2021 and Guido likes the idea to make static methods callable. See also PR #25268 which makes staticmethod more "usable" to be used directly as a function.
It's a tricky topic. We could also require to always use staticmethod when using directly a function as a method:
class MyClass:
method = staticmethod(func)
But sometimes, we need functions which can be used directly as method without staticmethod(), to mimick built-in function. Well see https://bugs.python.org/issue43682 and https://bugs.python.org/issue20309 discussions ;-)
staticmethod is a simple thing. It is only purpose to decorate functions before setting them as class attribute if we do not want to make them instance methods. There is no use cases for calling staticmethod object.
Victor want to replace OpenWrapper with staticmethod(open) to keep builtin open a non-descriptor just for the case if some user code sets it as class attribute. This is very special case, and I think that it would be better to apply staticmethod immediately before setting a class attribute:
class MyClass:
open = staticmethod(open)
In any case staticmethod is not perfect replacement of OpenWrapper. If we go this way, we should at least implement __doc__ for staticmethod, and preferably __repr__, __name__, __module__, __qualname__, __text_signature__, etc, etc. It is a big issue.
In any case staticmethod is not perfect replacement of OpenWrapper. If we go this way, we should at least implement doc for staticmethod, and preferably repr, name, module, qualname, text_signature, etc, etc. It is a big issue.
I solved this in PR #25268, did you see my PR?
This is very special case
See also https://bugs.python.org/issue43682#msg389907:
My usecase is to avoid any behavior difference between io.open and _pyio.open functions: PEP 399 "Pure Python/C Accelerator Module Compatibility Requirements". Currently, this is a very subtle difference when it's used to define a method.
It's a similar issue than PEP 570 (positional-only arguments) solved for Python re-implementation of a C extension. We should either prevent C extensions to behave than Python, or we should allow pure Python code to behave the same.
Here the issue is complex (changing built-in functions or Python functions to add/remove descriptor), and I propose to only change staticmethod(). In the whole stdlib, I'm only aware of io.open which is used sometimes directly to define a method (without @staticmethod). But the issue happens with any built-in function whch is reimplemented in Python (e.g. in PyPy).
I merged my PR #25268 and rebased this PR on top of it.
By the way, just for consistency, should we also make class methods callable? I have no use case for that :-)
By the way, just for consistency, should we also make class methods
callable? I have no use case for that :-) I thought those are callable already.
Guido:
I thought those are callable already.
Me too, but hey, static methods and class methods are weird!
$ python3
Python 3.9.2 (default, Feb 20 2021, 00:00:00)
>>> def func(): pass
...
>>> wrapper = classmethod(func)
>>> wrapper()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'classmethod' object is not callable
It's only callable when it goes throught the descriptor!
Well, ignore my idea of making class methods callable. It makes no sense since it doesn't pass the class in this case. Example:
def func(cls):
print(cls)
class MyClass:
method = classmethod(func)
MyClass.method()
MyClass().method()
MyClass.__dict__['method']()
The last call fails because it doesn't go trought the descriptor, and so the class method doesn't get the class argument.
Output:
vstinner@apu$ python3 x.py
<class '__main__.MyClass'>
<class '__main__.MyClass'>
Traceback (most recent call last):
File "/home/vstinner/python/master/x.py", line 9, in <module>
MyClass.__dict__['method']()
TypeError: 'classmethod' object is not callable
Well, ignore my idea of making class methods callable. It makes no sense since it doesn't pass the class in this case.
You should pass that in explicitly in that case. I guess calling classmethod(f)(C) could do the same thing as classmethod(f).__func__(C). But yeah, this doesn't seem as important as making staticmethod(f) callable, because there's nothing complicated there.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But please first fix the comment about class methods being callable.
Comment on lines 99 to 100
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Class methods aren't callable yet.
Static methods (@staticmethod) are now callable as regular functions.
I fixed test_pydoc which shows a nice enhancement of this PR ;-)
self.assertEqual(self._get_summary_lines(X.__dict__['sm']),
- 'sm(...)\n'
+ 'sm(x, y)\n'
' A static method\n')
With this PR, inspect.signature() works on a static method, and returns the same signature than the wrapped callable object.
$ ./python
Python 3.10.0a7+
>>> def func(x: int, y: int) -> float: pass
...
>>> import inspect
>>> inspect.signature(func)
<Signature (x: int, y: int) -> float>
>>> wrapper=staticmethod(func)
>>> inspect.signature(wrapper)
<Signature (x: int, y: int) -> float>
On Python 3.9 (and in master without this change), inspect.signature(wrapper) fails with "TypeError: <staticmethod...> is not a callable object".
vstinner
deleted the
callable_staticmethod
branch
It is possible to wrap a static method into a new static method, staticmethod(staticmethod(func)) or put two @staticmethod decorators on the same function. It is inefficient, but I don't think that the staticmethod() constructor must return the first static method unchanged, since static methods are mutable.
Python 3.10.0a7+
>>> def func(): return 5
...
>>> wrapper1 = staticmethod(func)
>>> wrapper2 = staticmethod(wrapper1)
>>> wrapper1
<staticmethod(<function func at 0x7fffea3071d0>)>
>>> wrapper2
<staticmethod(<staticmethod(<function func at 0x7fffea3071d0>)>)>
>>> wrapper2()
5
>>> wrapper2.x=1
>>> wrapper1.x
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'staticmethod' object has no attribute 'x'
Moreover, I expect that most decorators have a similar issue. Maybe a linter or a debug check can warn on that, but I don't think that staticmethod() must raise an error or return the first (static method) wrapper unchanged.