- cpython/Objects/classobject.c
- cpython/Include/classobject.h
the PyMethodObject represents the type method in c-level
class C(object):
def f1(self, val):
return val
>>> c = C()
>>> type(c.f1)
<class 'method'>
the layout of c.f1
as you can see from the layout, field im_func stores the function object that implements the method
>>> C.f1
<function C.f1 at 0x10b80f040>
field im_self stores the instance object this method bound to
>>> c
<__main__.C object at 0x10b7cbcd0>
when you call
>>> c.f1(123)
123
the PyMethodObject delegate the real call to im_func with im_self as the first argument
static PyObject *
method_call(PyObject *method, PyObject *args, PyObject *kwargs)
{
PyObject *self, *func;
/* get im_self */
self = PyMethod_GET_SELF(method);
if (self == NULL) {
PyErr_BadInternalCall();
return NULL;
}
/* get im_func */
func = PyMethod_GET_FUNCTION(method);
/* call im_func with im_self as the first argument */
return _PyObject_Call_Prepend(func, self, args, kwargs);
}
static PyMethodObject *free_list;
static int numfree = 0;
#ifndef PyMethod_MAXFREELIST
#define PyMethod_MAXFREELIST 256
#endif
free_list is a single linked list, it's used for PyMethodObject to safe malloc/free overhead
im_self field is used to chain the element
the PyMethodObject will be created when you trying to access the bound-method, not when the instance is created
>>> c1 = C()
>>> id(c1)
4514815184
>>> c2 = C()
>>> id(c2)
4514815472
>>> id(c1.f1) # c1.f1 is created in this line, after this line, the reference count of c1.f1 becomes 0 and c1.f1 deallocated
4513259240
>>> id(c1.f1) # the id is resued
4513259240
>>> id(c2.f1)
4513259240
now, let's see an example of free_list
>>> c1_f1_1 = c1.f1
>>> c1_f1_2 = c1.f1
>>> id(c1_f1_1)
4529762024
>>> id(c1_f1_2)
4529849392
assume the free_list is empty now
>>> del c1_f1_1
>>> del c1_f1_2
>>> c1_f1_3 = c1.f1
>>> id(c1_f1_3)
4529849392
let's define an object with classmethod and staticmethod
class C(object):
def f1(self, val):
return val
@staticmethod
def fs():
pass
@classmethod
def fc(cls):
return cls
>>> c1 = C()
>>> type(c1.fs)
<class 'function'>
>>> type(c1.fc)
<class 'method'>
the @classmethod keeps type of c1.fc as method
c1.fc is another instance of PyMethodObject, with im_func bind to the actual callable object, and im_self bind to the <class '__main__.C'>
>>> C
<class '__main__.C'>
how classmethod work internally?
classmethod is a type in python3
typedef struct {
PyObject_HEAD
PyObject *cm_callable;
PyObject *cm_dict;
} classmethod;
let's see what's under the hood
fc = classmethod(lambda self : self)
class C(object):
fc1 = fc
>>> cc = C()
>>> type(fc)
>>> <class 'classmethod'>
>>> type(cc.fc1)
>>> <class 'method'>
>>> fc.__dict__["b"] = "c"
>>> cc.fc1
<bound method <lambda> of <class '__main__.C'>>
get a different result when access the same object in a different way, why?
when you trying to access the fc1 in instance cc, the descriptor protocol will try several different paths to get the attribute in the following step
- call
__getattribute__
of the object C C.__dict__["fc1"]
is a data descriptor?- yes, return
C.__dict__['fc1'].__get__(instance, Class)
- no, return
cc.__dict__['fc1']
if 'fc1' incc.__dict__
elseC.__dict__['fc1'].__get__(instance, Class)
if hasattr(C.__dict__['fc1']
,__get__
) elseC.__dict__['fc1']
- yes, return
- if not found in above steps, call
c.__getattr__("fc1")
for more detail, please refer to this blog object-attribute-lookup or descr object
because classmethod implements __get__
and __set__
, it's a data descriptor, when you try to access attribute cc.fc1, you will actually call fc1.__get__
, and caller will get whatever it returns
we can see the __get__
function of classmethod object(defined as cm_descr_get
in C)
static PyObject *
cm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
classmethod *cm = (classmethod *)self;
if (cm->cm_callable == NULL) {
PyErr_SetString(PyExc_RuntimeError,
"uninitialized classmethod object");
return NULL;
}
if (type == NULL)
type = (PyObject *)(Py_TYPE(obj));
return PyMethod_New(cm->cm_callable, type);
}
when you access fc1 by cc.fc1, the descriptor protocol will call the function above, which returns whatever in the cm_callable, wrapped by PyMethod_New() function, which makes the return object a new bounded-PyMethodObject
the @staticmethod changes type of c1.fs to function
>>> type(c1.fs)
<class 'function'>
typedef struct {
PyObject_HEAD
PyObject *sm_callable;
PyObject *sm_dict;
} staticmethod;
this is the layout of staticmethod object
fs = staticmethod(lambda : None)
class C(object):
fs1 = fs
>>> fs.__dict__["a"] = "b"
>>> cc = C()
>>> type(fs)
>>> <class 'staticmethod'>
>>> type(cc.fs1)
>>> <class 'function'>
>>> cc.fs1
<function <lambda> at 0x1047d9f40>
we can see the __get__
function of staticmethod object
static PyObject *
sm_descr_get(PyObject *self, PyObject *obj, PyObject *type)
{
staticmethod *sm = (staticmethod *)self;
if (sm->sm_callable == NULL) {
PyErr_SetString(PyExc_RuntimeError,
"uninitialized staticmethod object");
return NULL;
}
Py_INCREF(sm->sm_callable);
return sm->sm_callable;
}
so, when you access fs1 by cc.fs1, the descriptor protocol happens again, C.__dict__["fs1"]__get__(instance, Class)
returns the lambda function