When defining methods in a class we do have an argument for the object on which the method is invoked i.e self.
Say I have a class Foo
, and an instance of it:
class Foo:
def m(self):
pass
foo = Foo()
and we do something like this:
foo_m = foo.m
and then try calling foo_m
with the same parameters the way would have done with foo.m
. It just works.
So how does foo_m
knows what should be the value of the self argument when it is not being called in foo.m()
fashion?
When printing the foo.m
, the REPL shows that its a bound method
to that particular object.
Is it some other function object that keeps track of the self
argument and the method to be called?
2 Answers 2
A bound method is just a Python object that stores self
in one of its slots. Take a look at methodobject.c
for the C implementation. In Python the slot is accessible via the __self__
attribute:
>>> a = [1, 2, 3].append
>>> a
<built-in method append of list object at 0x103b39588>
>>> a.__self__
[1, 2, 3]
This is because of the descriptor protocol (the same that underlies @property
).
Very simply*, if you do foo.m
, it works roughly like calling getattribute(foo, 'm')
in the following pseudocode:
def getattribute(obj, attr):
if attr in obj.__dict__:
# instance variable
return obj.__dict__[attr]
# class variable
for cls in type(obj).__mro__:
# MRO: method resolution order, sequence of the type and all its superclasses
if attr in cls.__dict___:
value = cls.__dict__[attr]
break
else: # no break
raise AttributeError(attr)
if hasattr(value, '__get__'):
# descriptor protocol gets invoked!
return value.__get__(obj, type(obj))
# regular class variable
return value
* This is only valid if obj
is not a type, and if there is no __getattr__
or __getattribute__
involved, but I'm ignoring those possibilities because they complicate the story but aren't very relevant for your particular question.
Additionally, any occurrence of attribute access inside of getattribute
is actually not regular attribute access, so there's no infinite regression.
Methods work because all Python functions are descriptors themselves, and they work a bit like this:
class FunctionType:
def __call__(self, *args, **kwargs):
... # don't think about this one too hard
def __get__(self, instance, class_):
if instance is None: # Foo.m
return self
return BoundFunction(__func__=self, __self__=instance) # foo.m
@dataclass
class BoundFunction:
__func__: FunctionType
__self__: Any
def __call__(self, *args, **kwargs):
# because self.__func__ is an instance variable, it doesn't invoke the descriptor protocol!
return self.__func__(self.__self__, *args, **kwargs)
foo_m
is bound tofoo.m
and therefor also to thefoo
object, when it is called it internally usesfoo
as the self parameter.functools.partial
, whereself
is already set to the instance and you just supply the other parameters.