forked from zpoint/CPython-Internals
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclass.md
More file actions
282 lines (199 loc) · 8.4 KB
/
Copy pathclass.md
File metadata and controls
282 lines (199 loc) · 8.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# class
# contents
* [related file](#related-file)
* [memory layout](#memory-layout)
* [fields](#fields)
* [im_func](#im_func)
* [im_self](#im_self)
* [free_list](#free_list)
* [classmethod and staticmethod](#classmethod-and-staticmethod)
* [classmethod](#classmethod)
* [staticmethod](#staticmethod)
# related file
* cpython/Objects/classobject.c
* cpython/Include/classobject.h
# memory layout
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'>

# fields
the layout of **c.f1**

## im_func
as you can see from the layout, field **im_func** stores the [function](https://github.com/zpoint/CPython-Internals/blob/master/BasicObject/func/func.md) object that implements the method
>>> C.f1
<function C.f1 at 0x10b80f040>
## im_self
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);
}
# free_list
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

# classmethod and staticmethod
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'>
## classmethod
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' in `cc.__dict__` else
* `C.__dict__['fc1'].__get__(instance, Class)` if hasattr(`C.__dict__['fc1']`, `__get__`) else `C.__dict__['fc1']`
* if not found in above steps, call `c.__getattr__("fc1")`

for more detail, please refer to this blog [object-attribute-lookup](https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/#object-attribute-lookup) or [descr object](https://github.com/zpoint/CPython-Internals/blob/master/Interpreter/descr/descr.md)

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
## staticmethod
the **@staticmethod** changes type of **c1.fs** to [function](https://github.com/zpoint/CPython-Internals/blob/master/BasicObject/func/func.md)
>>> 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