20-Python类机制的深度解析(第四部分): 实例对象的创建、以及属性访问 楔子 介绍完类对象之后,我们来介绍实例对象。我们之前费了老鼻子劲将类对象剖析了一遍,但这仅仅是万里长征的第一步。因为Python虚拟机执行时,在内存中兴风作浪的是一个个的实例对象,而类对象只是幕后英雄。
通过class类对象创建实例对象 我们还以之前的代码为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Girl : name = "夏色祭" def __init__ (self ): print ("__init__" ) def f (self ): print ("f" ) def g (self, name ): self.name = name print (self.name) girl = Girl()
看一下它的字节码,这里我们只看创建实例对象的字节码,也就是模块的字节码。
1 2 3 4 5 6 7 8 9 10 11 12 13 1 0 LOAD_BUILD_CLASS 2 LOAD_CONST 0 (<code object Girl at 0x000002B7A85FABE0 , file "instance" , line 1 >) 4 LOAD_CONST 1 ('Girl' ) 6 MAKE_FUNCTION 0 8 LOAD_CONST 1 ('Girl' ) 10 CALL_FUNCTION 2 12 STORE_NAME 0 (Girl) 15 14 LOAD_NAME 0 (Girl) 16 CALL_FUNCTION 0 18 STORE_NAME 1 (girl) 20 LOAD_CONST 2 (None) 22 RETURN_VALUE
我们看到在类构建完毕之后,14 LOAD_NAME这条指令便将刚刚构建的类Girl取了出来、压入运行时栈,然后通过CALL_FUNCTION将栈里面的类弹出、进行调用,得到实例对象,再将实例对象设置在栈顶。18 STORE_NAME将栈顶的实例对象弹出,让符号girl与之绑定,放在local空间中。
所以我们看到调用类对象的指令居然也是CALL_FUNCTION,因为一开始我们说了,类和函数一样,都是要先将PyCodeObject变成PyFunctionObject。
因此执行完毕之后,模块的local空间就会变成这样:
在CALL_FUNCTION中,Python同样会执行对应类型的tp_call操作。所以创建实例的时候,显然执行PyType_Type的tp_call,因此最终是在PyType_Type.tp_call中调用Girl.tp_new来创建instance对象的。
需要注意的是,在创建class Girl这个对象时,Python虚拟机调用PyType_Ready对class Girl进行了初始化,其中一项动作就是继承基类,所以Girl.tp_new实际上就是object.tp_new,而在PyBaseObject_Type中,这个操作被定义为object_new。创建class对象和创建instance对象的不同之处正是在于tp_new不同。创建class对象,Python虚拟机使用的是tp_new,创建instance对象,Python虚拟机则使用object_new。使用类重写__new__的话,应该很容易明白。
因此,由于我们创建的不是class对象,而是instance对象,type_call会尝试进行初始化的动作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static PyObject *type_call (PyTypeObject *type, PyObject *args, PyObject *kwds) { type = Py_TYPE(obj); if (type->tp_init != NULL ) { int res = type->tp_init(obj, args, kwds); if (res < 0 ) { assert(PyErr_Occurred()); Py_DECREF(obj); obj = NULL ; } else { assert(!PyErr_Occurred()); } } return obj; }
那么这个tp_init是哪里来的的,是在使用tp_new创建类对象的时候来的,tp_init在PyType_Ready时会继承PyBaseObject_Type的object_init操作。但正如我们之前说的那样,因为A中的定义重写了__init__,所以在 *fixup_slot_dispatchers* 中,tp_init会指向slotdef中指定的与__init__对应的slot_tp_init。并且还会设置tp_alloc,这与内存分配有关,源码中会有所体现。
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 static PyObject *type_new (PyTypeObject *metatype, PyObject *args, PyObject *kwds) { type->tp_alloc = PyType_GenericAlloc; if (type->tp_flags & Py_TPFLAGS_HAVE_GC) { type->tp_free = PyObject_GC_Del; type->tp_traverse = subtype_traverse; type->tp_clear = subtype_clear; } else type->tp_free = PyObject_Del; fixup_slot_dispatchers(type); } PyObject * PyType_GenericAlloc (PyTypeObject *type, Py_ssize_t nitems) { PyObject *obj; const size_t size = _PyObject_VAR_SIZE(type, nitems+1 ); if (PyType_IS_GC(type)) obj = _PyObject_GC_Malloc(size); else obj = (PyObject *)PyObject_MALLOC(size); if (obj == NULL ) return PyErr_NoMemory(); memset (obj, '\0' , size); if (type->tp_itemsize == 0 ) (void )PyObject_INIT(obj, type); else (void ) PyObject_INIT_VAR((PyVarObject *)obj, type, nitems); if (PyType_IS_GC(type)) _PyObject_GC_TRACK(obj); return obj; }
而在 *slot_tp_init* 中又做了哪些事情呢?
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 static int slot_tp_init (PyObject *self, PyObject *args, PyObject *kwds) { _Py_IDENTIFIER(__init__); int unbound; PyObject *meth = lookup_method(self, &PyId___init__, &unbound); PyObject *res; if (meth == NULL ) return -1 ; if (unbound) { res = _PyObject_Call_Prepend(meth, self, args, kwds); } else { res = PyObject_Call(meth, args, kwds); } Py_DECREF(meth); if (res == NULL ) return -1 ; if (res != Py_None) { PyErr_Format(PyExc_TypeError, "__init__() should return None, not '%.200s'" , Py_TYPE(res)->tp_name); Py_DECREF(res); return -1 ; } Py_DECREF(res); return 0 ; }
所以如果你在定义class时,重写了__init__函数,那么创建实例对象时搜索的结果就是你写的函数,如果没有重写那么执行object的__init__操作,而在object的__init__中,Python虚拟机则什么也不做,而是直接返回。
到了这里可以小结一下,从class对象创建instance对象的两个步骤:
instance = class.__new__(class, *args, **kwargs)
class.__init__(instance, *args, **kwargs)
需要注意的是,这两个步骤同样也适用于从metaclass对象创建class对象,因为从metaclass对象创建class对象的过程其实和class对象创建instance对象是一样的,我们说class具有二象性。
访问instance对象中的属性 在前面的章节中我们讨论名字空间时就提到,在Python中,形如x.y形式的表达式称之为”属性引用”,其中x为对象,y为对象的某个属性,这个属性可以是很多种,比如:整数、字符串、函数、类、甚至是模块等等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Girl : name = "夏色祭" def __init__ (self ): print ("__init__" ) def f (self ): print ("f" ) def g (self, name ): self.name = name print (self.name) girl = Girl() girl.f() girl.g("神乐mea" )
我们加上属性查找逻辑,看看它的字节码如何。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1 0 LOAD_BUILD_CLASS 2 LOAD_CONST 0 (<code object Girl at 0x0000019158F5ABE0 , file "instance" , line 1 >) 4 LOAD_CONST 1 ('Girl' ) 6 MAKE_FUNCTION 0 8 LOAD_CONST 1 ('Girl' ) 10 CALL_FUNCTION 2 12 STORE_NAME 0 (Girl) 15 14 LOAD_NAME 0 (Girl) 16 CALL_FUNCTION 0 18 STORE_NAME 1 (girl) 16 20 LOAD_NAME 1 (girl) 22 LOAD_METHOD 2 (f) 24 CALL_METHOD 0 26 POP_TOP 17 28 LOAD_NAME 1 (girl) 30 LOAD_METHOD 3 (g) 32 LOAD_CONST 2 ('神乐mea' ) 34 CALL_METHOD 1 36 POP_TOP 38 LOAD_CONST 3 (None) 40 RETURN_VALUE
20 LOAD_NAME: 加载变量girl, 因为是girl.f, 所以首先要把girl加载进来, 也就是压入运行时栈;
22 LOAD_METHOD: 我们看到了一个新的指令, LOAD_METHOD, 显然这是加载一个方法, 关于函数和方法的区别我们后面会详细说;
24 CALL_METHOD: 调用方法;
26 POP_TOP: 从栈顶将元素弹出;
32 LOAD_CONST: 除了加载girl和g之外, 还要加载一个常量字符串;
34 CALL_METHOD: 调用方法, 这里参数是1个;
所以关键指令就在于LOAD_METHOD和CALL_METHOD,我们先来看看LOAD_METHOD都做了什么吧。
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 case TARGET (LOAD_METHOD) : { PyObject *name = GETITEM(names, oparg); PyObject *obj = TOP(); PyObject *meth = NULL ; int meth_found = _PyObject_GetMethod(obj, name, &meth); if (meth == NULL ) { goto error; } if (meth_found) { SET_TOP(meth); PUSH(obj); } else { SET_TOP(NULL ); Py_DECREF(obj); PUSH(meth); } DISPATCH(); }
获取方法是LOAD_METHOD,那么获取属性呢?对,其实肯定有人想到了,获取属性是LOAD_ATTR。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 case TARGET (LOAD_ATTR) : { PyObject *name = GETITEM(names, oparg); PyObject *owner = TOP(); PyObject *res = PyObject_GetAttr(owner, name); Py_DECREF(owner); SET_TOP(res); if (res == NULL ) goto error; DISPATCH(); }
LOAD_ATTR和LOAD_METHOD这两个指令集我们都看到了,但是里面具体实现的方法还没有看,LOAD_ATTR调用了 *PyObject_GetAttr* 函数,LOAD_METHOD调用了 *_PyObject_GetMethod* ,我们来看看这两个方法都长什么样子。首先就从 *PyObject_GetAttr* 开始。
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 PyObject * PyObject_GetAttr (PyObject *v, PyObject *name) { PyTypeObject *tp = Py_TYPE(v); if (!PyUnicode_Check(name)) { PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'" , name->ob_type->tp_name); return NULL ; } if (tp->tp_getattro != NULL ) return (*tp->tp_getattro)(v, name); if (tp->tp_getattr != NULL ) { const char *name_str = PyUnicode_AsUTF8(name); if (name_str == NULL ) return NULL ; return (*tp->tp_getattr)(v, (char *)name_str); } PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%U'" , tp->tp_name, name); return NULL ; }
在Python的class对象中,定义了两个与属性访问相关的操作:tp_getattro和tp_getattr。其中tp_getattro是优先选择的属性访问动作,而tp_getattr在Python中已不推荐使用。而这两者的区别在 *PyObject_GetAttr* 中已经显示的很清楚了,主要是在属性名的使用上,tp_getattro所使用的属性名必须是一个PyUnicodeObject对象,而tp_getattr所使用的属性名必须是一个char *。因此如果某个类型定义了tp_getattro和tp_getattr,那么 *PyObject_GetAttr* 优先使用tp_getattro,因为这位老铁写在上面。
在Python虚拟机创建class Girl时,会从PyBaseObject_Type中继承其tp_getattro->PyObject_GenericGetAttr
,所以Python虚拟机又会在这里进入 *PyObject_GenericGetAttr* ,并且 *PyObject_GenericGetAttr* 正好涉及到了Python中的描述符,因此也为我们我们后面介绍描述符埋下了一个伏笔。
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 PyObject * PyObject_GenericGetAttr (PyObject *obj, PyObject *name) { return _PyObject_GenericGetAttrWithDict(obj, name, NULL , 0 ); } PyObject * _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict, int suppress) { PyTypeObject *tp = Py_TYPE(obj); PyObject *descr = NULL ; PyObject *res = NULL ; descrgetfunc f; Py_ssize_t dictoffset; PyObject **dictptr; if (!PyUnicode_Check(name)){ PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'" , name->ob_type->tp_name); return NULL ; } Py_INCREF(name); if (tp->tp_dict == NULL ) { if (PyType_Ready(tp) < 0 ) goto done; } descr = _PyType_Lookup(tp, name); f = NULL ; if (descr != NULL ) { Py_INCREF(descr); f = descr->ob_type->tp_descr_get; if (f != NULL && PyDescr_IsData(descr)) { res = f(descr, obj, (PyObject *)obj->ob_type); if (res == NULL && suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) { PyErr_Clear(); } goto done; } } if (dict == NULL ) { dictoffset = tp->tp_dictoffset; if (dictoffset != 0 ) { if (dictoffset < 0 ) { Py_ssize_t tsize; size_t size; tsize = ((PyVarObject *)obj)->ob_size; if (tsize < 0 ) tsize = -tsize; size = _PyObject_VAR_SIZE(tp, tsize); _PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX); dictoffset += (Py_ssize_t)size; _PyObject_ASSERT(obj, dictoffset > 0 ); _PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0 ); } dictptr = (PyObject **) ((char *)obj + dictoffset); dict = *dictptr; } } if (dict != NULL ) { Py_INCREF(dict); res = PyDict_GetItemWithError(dict, name); if (res != NULL ) { Py_INCREF(res); Py_DECREF(dict); goto done; } else { Py_DECREF(dict); if (PyErr_Occurred()) { if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) { PyErr_Clear(); } else { goto done; } } } } if (f != NULL ) { res = f(descr, obj, (PyObject *)Py_TYPE(obj)); if (res == NULL && suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) { PyErr_Clear(); } goto done; } if (descr != NULL ) { res = descr; descr = NULL ; goto done; } if (!suppress) { PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%U'" , tp->tp_name, name); } done: Py_XDECREF(descr); Py_DECREF(name); return res; }
属性访问是从 *PyObject_GetAttr* 开始,那么下面我们来看看 *_PyObject_GetMethod* 生的什么模样,其实不用想也知道,它和 *PyObject_GetAttr* 高度相似。
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 int _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) { PyTypeObject *tp = Py_TYPE(obj); PyObject *descr; descrgetfunc f = NULL ; PyObject **dictptr, *dict; PyObject *attr; int meth_found = 0 ; assert(*method == NULL ); if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr || !PyUnicode_Check(name)) { *method = PyObject_GetAttr(obj, name); return 0 ; } if (tp->tp_dict == NULL && PyType_Ready(tp) < 0 ) return 0 ; descr = _PyType_Lookup(tp, name); if (descr != NULL ) { Py_INCREF(descr); if (PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) { meth_found = 1 ; } else { f = descr->ob_type->tp_descr_get; if (f != NULL && PyDescr_IsData(descr)) { *method = f(descr, obj, (PyObject *)obj->ob_type); Py_DECREF(descr); return 0 ; } } } dictptr = _PyObject_GetDictPtr(obj); if (dictptr != NULL && (dict = *dictptr) != NULL ) { Py_INCREF(dict); attr = PyDict_GetItemWithError(dict, name); if (attr != NULL ) { Py_INCREF(attr); *method = attr; Py_DECREF(dict); Py_XDECREF(descr); return 0 ; } else { Py_DECREF(dict); if (PyErr_Occurred()) { Py_XDECREF(descr); return 0 ; } } } if (meth_found) { *method = descr; return 1 ; } if (f != NULL ) { *method = f(descr, obj, (PyObject *)Py_TYPE(obj)); Py_DECREF(descr); return 0 ; } if (descr != NULL ) { *method = descr; return 0 ; } PyErr_Format(PyExc_AttributeError, "'%.50s' object has no attribute '%U'" , tp->tp_name, name); return 0 ; }
非常类似,这里就不介绍了。
实例对象的属性字典 在属性访问的时候,我们可以通过girl.__dict__这种形式访问。但是这就奇怪了,在之前的描述中,我们看到从class Girl创建instance girl的时候,Python并没有为instance创建PyDictObject对象啊。
但是在上一篇介绍metaclass的时候,我们说过这样一句话,对于任意继承object的class对象来说,这个大小为PyBaseObject_Type->tp_basicsize + 16
,其中的16是2 * sizeof(PyObject *)。后面跟着的两个PyObject *的空间被设置给了tp_dictoffset和tp_weaklistoffset,那么现在是时候揭开谜底了。
在创建class类对象时我们曾说,Python虚拟机设置了一个名为tp_dictoffset的域,从名字推断,这个可能就是instance对象中__dict__的偏移位置。
虚线中画出的dict对象就是我们期望中的实例对象的属性字典,这个猜想可以在PyObject_GenericGetAttr中得到证实。
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 PyObject * PyObject_GenericGetAttr (PyObject *obj, PyObject *name) { return _PyObject_GenericGetAttrWithDict(obj, name, NULL , 0 ); } PyObject * _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict, int suppress) { if (dict == NULL ) { dictoffset = tp->tp_dictoffset; if (dictoffset != 0 ) { if (dictoffset < 0 ) { Py_ssize_t tsize; size_t size; tsize = ((PyVarObject *)obj)->ob_size; if (tsize < 0 ) tsize = -tsize; size = _PyObject_VAR_SIZE(tp, tsize); assert(size <= PY_SSIZE_T_MAX); dictoffset += (Py_ssize_t)size; assert(dictoffset > 0 ); assert(dictoffset % SIZEOF_VOID_P == 0 ); } dictptr = (PyObject **) ((char *)obj + dictoffset); dict = *dictptr; } }
如果dictoffset小于0,意味着Girl是继承自类似str这样的变长对象,Python虚拟机会对dictoffset做一些处理,最终仍然会使dictoffset指向a的内存中额外申请的位置。而PyObject_GenericGetAttr正是根据这个dictoffset获得了一个dict对象。更近一步,我们发现函数g中有设置self.name的代码,这个instance对象的属性设置也会访问属性字典,而这个设置的动作最终会调用 *PyObject_GenericSetAttr* ,也就是girl.__dict__
最初被创建的地方。
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 int PyObject_GenericSetAttr (PyObject *obj, PyObject *name, PyObject *value) { return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL ); } int _PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name, PyObject *value, PyObject *dict) { PyTypeObject *tp = Py_TYPE(obj); PyObject *descr; descrsetfunc f; PyObject **dictptr; int res = -1 ; if (!PyUnicode_Check(name)){ PyErr_Format(PyExc_TypeError, "attribute name must be string, not '%.200s'" , name->ob_type->tp_name); return -1 ; } if (tp->tp_dict == NULL && PyType_Ready(tp) < 0 ) return -1 ; Py_INCREF(name); descr = _PyType_Lookup(tp, name); if (descr != NULL ) { Py_INCREF(descr); f = descr->ob_type->tp_descr_set; if (f != NULL ) { res = f(descr, obj, value); goto done; } } if (dict == NULL ) { dictptr = _PyObject_GetDictPtr(obj); if (dictptr == NULL ) { if (descr == NULL ) { PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%U'" , tp->tp_name, name); } else { PyErr_Format(PyExc_AttributeError, "'%.50s' object attribute '%U' is read-only" , tp->tp_name, name); } goto done; } res = _PyObjectDict_SetItem(tp, dictptr, name, value); } else { Py_INCREF(dict); if (value == NULL ) res = PyDict_DelItem(dict, name); else res = PyDict_SetItem(dict, name, value); Py_DECREF(dict); } if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError)) PyErr_SetObject(PyExc_AttributeError, name); done: Py_XDECREF(descr); Py_DECREF(name); return res; }
再论descriptor 前面我们看到,在 *PyType_Ready* 中,Python虚拟机会填充tp_dict,其中与操作名对应的是一个个descriptor(描述符),那时我们看到的是descriptor这个概念在Python内部是如何实现的。现在我们将要剖析的是descriptor在Python的类机制中究竟会起到怎样的作用。
在Python虚拟机对class对象或instance对象进行属性访问时,descriptor将对属性访问的行为产生重大的影响。一般而言,对于一个对象obj,如果obj.__class__
对应的class对象中存在__get__、__set__、__delete__
操作(不要求三者同时存在)
,那么obj便可以称之为描述符。在slotdefs中,我们会看到这三种魔法方法对应的操作。
1 2 3 4 5 6 7 8 9 TPSLOT("__get__" , tp_descr_get, slot_tp_descr_get, wrap_descr_get, "__get__($self, instance, owner, /)\n--\n\nReturn an attribute of instance, which is of type owner." ), TPSLOT("__set__" , tp_descr_set, slot_tp_descr_set, wrap_descr_set, "__set__($self, instance, value, /)\n--\n\nSet an attribute of instance to value." ), TPSLOT("__delete__" , tp_descr_set, slot_tp_descr_set, wrap_descr_delete, "__delete__($self, instance, /)\n--\n\nDelete an attribute of instance." ),
前面我看到了 *PyWrapperDescrObject* 、*PyMethodDescrObject* 等对象,它们对应的类对象中分别为tp_descr_get设置了wrapperdescr_get,method_get等函数,所以它们是当之无愧的descriptor。
另外如果细分,descriptor还可以分为两种。
关于python中的描述符,我这里有一篇博客写的很详细,对描述符机制不太懂的话可以先去看看,https://www.cnblogs.com/traditional/p/11714356.html。
data descriptor:数据描述符,对应的__class__中定义了__get__和__set__的descriptor
no data descriptor:非数据描述符,对应的__class__中只定义了__get__方法。
在Python虚拟机访问instance对象的属性时,descriptor的一个作用就是影响Python虚拟机对属性的选择。从 *PyObject_GenericGetAttr* 源码中可以看到,Python虚拟机会在instance对象自身的__dict__
中寻找属性,也会在instance对象对应的class对象的mro列表中寻找属性,我们将前一种属性称之为instance属性,后一种属性称之为class属性。在属性的选择上,有如下规律:
Python虚拟机优先按照instance属性、class属性的顺序选择属性,即instance属性优先于class属性
如果在class属性中发现同名的data descriptor,那么该descriptor会优先于instance属性被Python虚拟机选择
这两条规则在对属性进行设置时仍然会被严格遵守,换句话说,如果执行girl.value = 1
,而在Girl中出现了名为value的数据描述符,那么不好意思,会执行__set__
方法,如果是非数据描述符,那么就不再走__set__
了,而是设置属性,相当于a.__dict__['value'] = 1
。
所以,获取被描述符代理的属性时,会直接调用__get__方法。设置的话,会调用__set__。当然要考虑优先级的问题,至于优先级的问题是什么,这里就不再解释,强烈建立看我上面发的博客链接,对描述符的解析很详细。
函数变身 在Girl的成员f对应的def语句中,我们分明一个self参数,那么self在Python中是不是一个真正有效的参数呢?还是它仅仅只是一个语法意义是占位符而已?这一点可以从函数g中看到答案,在g中有这样的语句:self.name = name
,这条语句毫无疑问地揭示了self确实是一个实实在在的对象,所以表面上看起来f是一个不需要参数的函数,但实际上是一个货真价值的带参函数,只不过第一个参数自动帮你传递了。根据使用Python的经验,我们都知道,传递给self的就是实例本身。但是现在问题来了,这是怎么实现的呢?我们先再看一遍字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1 0 LOAD_BUILD_CLASS 2 LOAD_CONST 0 (<code object Girl at 0x0000019D7B4EABE0 , file "instance" , line 1 >) 4 LOAD_CONST 1 ('Girl' ) 6 MAKE_FUNCTION 0 8 LOAD_CONST 1 ('Girl' ) 10 CALL_FUNCTION 2 12 STORE_NAME 0 (Girl) 15 14 LOAD_NAME 0 (Girl) 16 CALL_FUNCTION 0 18 STORE_NAME 1 (girl) 16 20 LOAD_NAME 1 (girl) 22 LOAD_METHOD 2 (f) 24 CALL_METHOD 0 26 POP_TOP 17 28 LOAD_NAME 1 (girl) 30 LOAD_METHOD 3 (g) 32 LOAD_CONST 2 ('神乐mea' ) 34 CALL_METHOD 1 36 POP_TOP 38 LOAD_CONST 3 (None) 40 RETURN_VALUE
我们看一下:24 CALL_METHOD,我们说会将girl.f压入运行时栈,然后就执行CALL_METHOD指令了,注意这里的oparg是0,表示不需要参数(不需要我们传递参数)
。注意:这里是CALL_METHOD,不是CALL_FUNCTION。因此我们可以有两条路可走,一条是看看CALL_METHOD是什么,另一条是再研究一下PyFunctionObject。我们先来看看CALL_METHOD这个指令长什么样子吧。
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 case TARGET (CALL_METHOD) : { PyObject **sp, *res, *meth; sp = stack_pointer; meth = PEEK(oparg + 2 ); if (meth == NULL ) { res = call_function(tstate, &sp, oparg, NULL ); stack_pointer = sp; (void )POP(); } else { res = call_function(tstate, &sp, oparg + 1 , NULL ); stack_pointer = sp; } PUSH(res); if (res == NULL ) goto error; DISPATCH(); } case TARGET (CALL_FUNCTION) : { PREDICTED(CALL_FUNCTION); PyObject **sp, *res; sp = stack_pointer; res = call_function(tstate, &sp, oparg, NULL ); stack_pointer = sp; PUSH(res); if (res == NULL ) { goto error; } DISPATCH(); }
通过对比,发现端倪,这两个都调用了call_function,但是传递的参数不一样,call_function的第二个参数一个oparg+1(猜测第一个给了self)
,一个是oparg,但是这还不足以支持我们找出问题所在。其实在剖析函数的时候,我们放过了PyFunctionObject的ob_type ->PyFunction_Type
。在这个PyFunction_Type中,隐藏着一个惊天大秘密。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PyTypeObject PyFunction_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0 ) "function" , sizeof (PyFunctionObject), func_descr_get, 0 , offsetof(PyFunctionObject, func_dict), 0 , 0 , func_new, };
我们发现 tp_descr_get 被设置成了func_descr_get,这意味着我们得到的是一个描述符。另外由于 girl.__dict__
中没有f,那么 girl.f 的返回值将会被 descriptor 改变,也就是 func_descr_get(Girl.f, girl, Girl)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static PyObject *func_descr_get (PyObject *func, PyObject *obj, PyObject *type) { if (obj == Py_None || obj == NULL ) { Py_INCREF(func); return func; } return PyMethod_New(func, obj); }
func_descr_get将Girl.f对应的PyFunctionObject进行了一番包装,所以通过PyMethod_New,Python虚拟机在PyFunctionObject的基础上创建一个新的对象PyMethodObject,那么这个PyMethodObject是什么呢?到PyMethod_New中一看,这个神秘的对象就现身了:
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 PyObject * PyMethod_New (PyObject *func, PyObject *self) { PyMethodObject *im; if (self == NULL ) { PyErr_BadInternalCall(); return NULL ; } im = free_list; if (im != NULL ) { free_list = (PyMethodObject *)(im->im_self); (void )PyObject_INIT(im, &PyMethod_Type); numfree--; } else { im = PyObject_GC_New(PyMethodObject, &PyMethod_Type); if (im == NULL ) return NULL ; } im->im_weakreflist = NULL ; Py_INCREF(func); im->im_func = func; Py_XINCREF(self); im->im_self = self; im->vectorcall = method_vectorcall; _PyObject_GC_TRACK(im); return (PyObject *)im; }
一切真相大白,原来那个神秘的对象就是PyMethodObject对象,看到free_list这样熟悉的字眼,我们就知道Python内部对PyMethodObject的实现和管理中使用缓冲池的技术。现在再来看看这个PyMethodObject:
1 2 3 4 5 6 7 8 9 10 11 typedef struct { PyObject_HEAD PyObject *im_func; PyObject *im_self; PyObject *im_weakreflist; vectorcallfunc vectorcall; } PyMethodObject;
在PyMethod_New中,分别将im_func,im_self设置了不同的值,分别是:f对应PyFunctionObject对象、实例girl对应的instance对象。因此通过PyMethodObject对象将PyFunctionObject对象和instance对象结合在一起,而这个PyMethodObject对象就是我们说的方法。
不管是类还是实例,获取成员函数都会走描述符的 func_descr_get,在里面会判断是类获取还是实例获取。如果是类获取,那么直接返回函数本身,如果实例获取则会通过PyMethod_New将func和instance绑定起来得到PyMethodObject对象,再调用函数的时候其实调用的是PyMethodObject。当调用PyMethodObject中会处理自动传参的逻辑,将instance和我们传递的参数组合起来(如果我们没有传参, 那么只有一个self)
,然后整体传递给PyFunctionObject,所以为什么实例调用方法的时候会自动传递第一个参数现在是真相大白了。
这个过程称之为成员函数的绑定,就是将实例和函数绑定起来,使之成为一个整体(方法)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Girl : name = "夏色祭" def __init__ (self ): print ("__init__" ) def f (self ): print ("f" ) def g (self, name ): self.name = name print (self.name) girl = Girl() print (Girl.f) print (girl.f) print (type (Girl.f)) print (type (girl.f))
我们看到通过类来调用成员的函数得到的就是一个普通的函数,如果是实例调用成员函数,那么会将成员函数包装成一个方法,也就是将成员函数和实例绑定在一起,得到结果就是方法,实现方式是通过描述符。
方法调用 在LOAD_METHOD指令结束之后,那么便开始了CALL_METHOD,我们知道这个和CALL_FUNCTION之间最大的区别就是,CALL_METHOD调用的是一个PyMethodObject对象,而CALL_FUNCTION调用的一个PyFunctionObject对象。
CALL_METHOD底层也调用了CALL_FUNCTION,因为方法是将函数和实例绑定在了一起,但最终执行的还是函数。
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 Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION call_function (PyThreadState *tstate, PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames) { if (tstate->use_tracing) { x = trace_call_function(tstate, func, stack , nargs, kwnames); } return x; } static PyObject *trace_call_function (PyThreadState *tstate, PyObject *func, PyObject **args, Py_ssize_t nargs, PyObject *kwnames) { PyObject *x; if (PyCFunction_Check(func)) { C_TRACE(x, _PyObject_Vectorcall(func, args, nargs, kwnames)); return x; } else if (Py_TYPE(func) == &PyMethodDescr_Type && nargs > 0 ) { PyObject *self = args[0 ]; func = Py_TYPE(func)->tp_descr_get(func, self, (PyObject*)Py_TYPE(self)); if (func == NULL ) { return NULL ; } C_TRACE(x, _PyObject_Vectorcall(func, args+1 , nargs-1 , kwnames)); Py_DECREF(func); return x; } return _PyObject_Vectorcall(func, args, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames); }
所以函数调用和方法调用本质上都是类似的,方法里面的成员im_func指向一个函数。调用方法的时候底层还是会调用函数,只不过在调用的时候会自动把方法里面的im_self作为第一个参数传到函数里面去。而我们通过类调用的时候,调用的就是一个普通的函数,所以第一个参数需要我们手动传递。
因此到了这里,我们可以在更高层次俯视一下Python的运行模型了,最核心的模型非常简单,可以简化为两条规则:
1. 在某个名字空间中寻找符号对应的对象
2. 对得到的对象进行某些操作
抛开面向对象这些花里胡哨的外表,其实我们发现class类对象其实就是一个名字空间,实例对象也是一个名字空间,不过这些名字空间通过一些特殊的规则连接在一起,使得符号的搜索过程变得复杂,从而实现了面向对象这种编程模式,それだけ。
bound method和unbound method 在Python中,当对作为方法(或者说作为属性的函数)
进行引用时,会有两种形式,bound method和unbound method。
bound method:这种形式是通过实例对象进行属性引用,就像我们之前说的a.f这样
unbound method:这种形式是通过类对象进行属性引用,比如A.f
在Python中,bound method和unbound method的本质区别就在于PyFunctionObject有没有和对象绑定在一起,成为PyMethodObject对象。bound method完成了绑定动作,而unbound method没有完成绑定动作。
所以无论是类还是实例,在调用成员函数的时候都会经过func_descr_get,但如果是类调用obj为NULL,实例对象调用obj就是实例。而obj如果为NULL,那么就直接返回了,否则通过PyMethod_New变成一个方法。
1 2 3 4 5 6 7 8 9 10 static PyObject *func_descr_get (PyObject *func, PyObject *obj, PyObject *type) { if (obj == Py_None || obj == NULL ) { Py_INCREF(func); return func; } return PyMethod_New(func, obj); }
我们通过Python演示一下:
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 class Descr : def __init__ (self, *args ): pass def __get__ (self, instance, owner ): print (instance) print (owner) class Girl : @Descr def f (self ): pass Girl.f """ None <class '__main__.Girl'> """ Girl().f """ <__main__.Girl object at 0x000001BDEE7A85E0> <class '__main__.Girl'> """
从Python的层面上我们也可以看到区别。
所以在对unbound method进行调用时,我们必须要显示的传递一个对象(这个对象可以任意,具体什么意思后面会演示)
作为成员函数的第一个参数,因为f无论如何都需要一个self参数,所以本质上就是Girl.f(girl)这种形式。而无论是对unbound method进行调用,还是对bound method进行调用,Python虚拟机的动作本质都是一样的,都是调用带位置参数的一般函数。区别只在于:当调用bound method时,由于Python虚拟机帮我们完成了PyFunctionObject对象和调用者的绑定,调用者将自动成为self参数;而调用unbound method时,没有这个绑定,我们需要自己传入self参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Girl (object ): def f (self ): print (self) girl = Girl() Girl.f(123 ) girl.f() print (Girl.f) print (girl.f)
注意:我们上面一直说的是调用者(其实说调用者也不是很准确)
,而不是实例对象,这是因为函数不仅可以和实例绑定,也可以和类绑定。
1 2 3 4 5 6 7 8 9 10 11 12 class Girl (object ): @classmethod def f (self ): print (self) print (Girl.f) print (Girl().f) Girl.f() Girl().f()
我们看到此时通过类去调用得到的不再是一个函数,而是一个方法,这是因为我们加上classmethod装饰器,当然classmethod也是一个描述符。当类在调用的时候,类也和函数绑定起来了,因此也会得到一个方法。不过被classmethod装饰之后,即使是实例调用,第一个参数传递的还是类本身,因为和 PyFunctionObject 绑定的是类、而不是实例。
所以得到的究竟是函数还是方法,就看这个函数有没有和某个对象进行绑定,只要绑定了,那么它就会变成方法。
千变万化的descriptor 当我们调用instance对象的函数时,最关键的一个动作就是从PyFunctionObject对象向PyMethodObject对象的转变,而这个关键的转变就取决于Python中的descriptor。当我们访问对象中的属性时,由于descriptor的存在,这种转换自然而然的就发生了。事实上,Python中的descriptor很强大,我们可以使用它做很多事情,而在Python的内部,也存在各种各样的descriptor,比如property、staticmethod、classmethod等等,这些descriptor给python的类机制赋予了强大的力量。具体源码就不分析了,我们直接通过Python代码的层面演示一下,这三种描述符的实现。
实现property
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Property : def __init__ (self, func ): self.func = func def __get__ (self, instance, owner ): if instance is None : return self res = self.func(instance) return res class A : @Property def f (self ): return "name: hanser" a = A() print (a.f) print (A.f)
总结:property是为了实例对象准备的,当然property支持的功能远不止我们上面演示的这么简单,它还可以进行set、delete,这些我们在介绍魔法方法的时候再说吧。
实现staticmethod
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class StaticMethod : def __init__ (self, func ): self.func = func def __get__ (self, instance, owner ): return self.func class A : @StaticMethod def f (): return "name: hanser" a = A() print (a.f()) print (A.f())
总结:staticmethod也是为了实例对象准备的,但是类也可以调用。
实现classmethod
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 class ClassMethod : def __init__ (self, func ): self.func = func def __get__ (self, instance, owner ): def inner (*args, **kwargs ): return self.func(owner, *args, **kwargs) return inner class A : name = "hanser" @ClassMethod def f (cls ): return f"name: {cls.name} " a = A() print (a.f()) print (A.f())
property
是 Python 中的一个内置装饰器,用于将类的方法转换为属性。它允许你以属性的形式访问和设置方法的值,而不是作为方法调用。这样可以让类的使用更加直观和简洁。property
可用于创建只读属性,或者在访问和修改属性时自定义行为,如进行验证或计算。
下面是一个使用 property
装饰器的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Circle : def __init__ (self, radius ): self._radius = radius @property def radius (self ): return self._radius @radius.setter def radius (self, value ): if value < 0 : raise ValueError("Radius cannot be negative." ) self._radius = value @property def diameter (self ): return self._radius * 2 @property def area (self ): return 3.14159 * self._radius ** 2
在这个例子中,我们创建了一个 Circle
类,包含 _radius
属性。我们使用 @property
装饰器定义了 radius
属性的 getter 和 setter 方法。这允许我们在设置半径时执行验证。同时,我们还定义了 diameter
和 area
属性,它们是基于半径的计算属性。这些属性只有 getter 方法,因此它们是只读的。通过使用 property
装饰器,我们可以像访问普通属性一样访问这些方法。
1 2 3 4 5 6 7 8 9 c = Circle(5 ) print (c.radius) print (c.diameter) print (c.area) c.radius = 10 print (c.radius) print (c.diameter) print (c.area)
@classmethod
@classmethod
将一个普通方法转换为类方法。类方法的第一个参数是类对象本身,通常命名为 cls
。这允许类方法访问和修改类级别的属性和方法。类方法可以在类的实例尚未创建时调用。此外,类方法可以被子类覆盖,这样子类就可以提供自己的实现。
1 2 3 4 5 python class Example: @classmethod def class_method(cls): print(f"Called class_method of {cls.__name__}")
@staticmethod
@staticmethod
将一个普通方法转换为静态方法。静态方法不接受特殊的第一个参数,也就是说它不接受类对象或实例对象。因此,静态方法不能访问类的属性和方法。静态方法只能访问它们所接收的参数和全局变量。静态方法通常用于实现与类和实例无关的功能。
1 2 3 4 5 python class Example: @staticmethod def static_method(arg): print(f"Called static_method with argument {arg}")
区别总结
参数:类方法的第一个参数是类对象本身(通常命名为 cls
),而静态方法没有特殊的第一个参数。
绑定:类方法绑定到类对象,可以访问和修改类级别的属性和方法。静态方法不绑定到类或实例,因此不能访问类或实例的属性和方法。
子类覆盖:类方法可以被子类覆盖,以提供特定于子类的实现。静态方法在子类中不会自动继承或覆盖。
用途:类方法通常用于创建和操作类及其实例,或实现可以被子类覆盖的工厂方法等功能。静态方法通常用于实现与类和实例无关的功能,类似于模块级别的函数。
在选择使用 @classmethod
还是 @staticmethod
时,你需要考虑方法的目的和需求。如果你需要访问或修改类级别的属性和方法,或者希望方法可以被子类覆盖,那么使用 @classmethod
更合适。如果方法只是执行一些独立的功能,并且不依赖于类或实例的属性和方法,那么使用 @staticmethod
更合适。
小结 这一次我们介绍了Python中实例对象的创建以及属性访问,下一篇我们介绍魔法方法。