Yes, Python creates new method objects for each access, because it builds a wrapper object to pass in self
. This is called a bound method.
Python uses descriptors to do this; function objects have a __get__
method that is called when accessed on a class:
>>> A.__dict__['m'].__get__(A(), A)
<bound method A.m of <__main__.A object at 0x10c29bc10>>
>>> A().m
<bound method A.m of <__main__.A object at 0x10c3af450>>
Note that Python cannot reuse A().m
; Python is a highly dynamic language and the very act of accessing .m
could trigger more code, which could alter behaviour of what A().m
would return next time when accessed.
The @classmethod
and @staticmethod
decorators make use of this mechanism to return a method object bound to the class instead, and a plain unbound function, respectively:
>>> class Foo:
... @classmethod
... def bar(cls): pass
... @staticmethod
... def baz(): pass
...
>>> Foo.__dict__['bar'].__get__(Foo(), Foo)
<bound method type.bar of <class '__main__.Foo'>>
>>> Foo.__dict__['baz'].__get__(Foo(), Foo)
<function Foo.baz at 0x10c2a1f80>
>>> Foo().bar
<bound method type.bar of <class '__main__.Foo'>>
>>> Foo().baz
<function Foo.baz at 0x10c2a1f80>
See the Python descriptor howto for more detail.
However, Python 3.7 adds a new LOAD_METHOD
- CALL_METHOD
opcode pair that replaces the current LOAD_ATTRIBUTE
- CALL_FUNCTION
opcode pair precisely to avoid creating a new method object each time. This optimisation transforms the executon path for instance.foo()
from type(instance).__dict__['foo'].__get__(instance, type(instance))()
with type(instance).__dict__['foo'](instance)
, so 'manually' passing in the instance directly to the function object. The optimisation falls back to the normal attribute access path (including binding descriptors) if the attribute found is not a pure-python function object.