《深度剖析CPython解释器》20. Python类机制的深度解析(第四部分): 实例对象的创建、以及属性访问
楔子
介绍完类对象之后,我们来介绍实例对象。我们之前费了老鼻子劲将类对象剖析了一遍,但这仅仅是万里长征的第一步。因为Python虚拟机执行时,在内存中兴风作浪的是一个个的实例对象,而类对象只是幕后英雄。
通过class类对象创建实例对象
我们还以之前的代码为例:
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 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会尝试进行初始化的动作。
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,这与内存分配有关,源码中会有所体现。
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
//........
//tp_alloc被设置为PyType_GenericAlloc, 表示为实例对象分配内存, 因为内存大小的元信息存在对应的类对象中
//并且在分配内存的同时会将实例对象的ob_type设置为对应的类对象
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;
//设置tp_init
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);
//设置实例对象的ob_type
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 中又做了哪些事情呢?
static int
slot_tp_init(PyObject *self, PyObject *args, PyObject *kwds)
{
_Py_IDENTIFIER(__init__);
int unbound;
//虚拟机会通过lookup_method从class对象及其mro列表中搜索属性__init__对应的操作
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;
//如果返回的不是None,那么报错,这个信息熟悉不
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为对象的某个属性,这个属性可以是很多种,比如:整数、字符串、函数、类、甚至是模块等等。
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 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都做了什么吧。
case TARGET(LOAD_METHOD): {
//从符号表中获取符号, 如果是girl.f的话, 那么这个name就是一个PyUnicodeObject对象"f"
PyObject *name = GETITEM(names, oparg);
//从栈顶获取(不是弹出, 弹出是POP)obj, 显然这个obj就是实例对象girl
PyObject *obj = TOP();
//meth是一个PyObject *指针,显然它要指向一个方法
PyObject *meth = NULL;
//这里是获取obj中和符号name绑定的方法,然后让meth指向它
//传入二级指针&meth,然后让meth存储的地址变成指向具体方法的地址
int meth_found = _PyObject_GetMethod(obj, name, &meth);
//如果meth == NULL,raise AttributeError
if (meth == NULL) {
/* Most likely attribute wasn't found. */
goto error;
}
//另外还返回了一个meth_found, 要么为1、要么为0
if (meth_found) {
//如果meth_found为1,说明meth是一个未绑定的方法,obj就是self
//关于绑定和未绑定我们后面会详细介绍
SET_TOP(meth);
PUSH(obj); // self
}
else {
//否则meth不是一个未绑定的方法,而是一个描述符协议返回的一个普通属性、亦或是其他的什么东西
//那么栈的第二个元素就会设置为NULL
SET_TOP(NULL);
Py_DECREF(obj);
PUSH(meth);
}
DISPATCH();
}
获取方法是LOAD_METHOD,那么获取属性呢?对,其实肯定有人想到了,获取属性是LOAD_ATTR。
case TARGET(LOAD_ATTR): {
//可以看到这个和LOAD_METHOD本质上是类似的,并且还要更简单一些
//name依旧是符号
PyObject *name = GETITEM(names, oparg);
//owner是所有者,为什么不叫obj,因为方法都是给实例用的,尽管类也能调用,但是方法毕竟是给实例用的
//但是属性的话,类和实例都可以访问,各自互不干扰,所以是owner
PyObject *owner = TOP();
//res显然就是获取属性返回的结果了, 通过PyObject_GetAttr进行获取
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 开始。
//Objects/object.c
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
//v: 对象
//name: 属性名
//获取类型对象
PyTypeObject *tp = Py_TYPE(v);
//name必须是一个字符串
if (!PyUnicode_Check(name)) {
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
name->ob_type->tp_name);
return NULL;
}
//通过类型对象的tp_getattro获取对应的属性, 实例获取属性(包括方法)的时候都是通过类来获取的
//girl.f()本质上就是Girl.f(girl), 但是后者是不是长得有点丑啊, 所以Python提供了girl.f()
//并且我们也看到了, 实例调用方法的时候会自动将自身作为参数传进去, 而类默认则不会
//也正因为如此类获取的话(Girl.f)叫函数, 实例获取(girl.f)的话叫方法, 后面会介绍
if (tp->tp_getattro != NULL)
return (*tp->tp_getattro)(v, name);
//通过tp_getattr获取属性对应的对象, 这里的name是一个char *, 而tp_getattro是一个PyObject *
//显然tp_getattro还可以处理中文的情况, 只不过我们不会使用中文来命名就是了
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中的描述符,因此也为我们我们后面介绍描述符埋下了一个伏笔。
//Objects/object.c
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)
{
//拿到obj的类型,对于我们的例子来说, 显然是class Girl
PyTypeObject *tp = Py_TYPE(obj);
//一个描述符对象
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr;
//name必须是str
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;
}
//尝试从mro列表中拿到符号对应的值,等价于descr = Girl.符号 if hasattr(Girl, '符号') else NULL
descr = _PyType_Lookup(tp, name);
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
//f = descr.__class__.__get__
f = descr->ob_type->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
//f不为NULL,并且descr是数据描述符,那么直接将描述符中__get__方法的结果返回
//这个f就是描述符里面的__get__方法,而这个descr就是描述符的一个实例对象
res = f(descr, obj, (PyObject *)obj->ob_type);
if (res == NULL && suppress &&
PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
}
goto done;
}
}
//那么显然要从instance对象自身的__dict__中寻找属性
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
//但如果dict为NULL,并且dictoffset不为0, 说明继承自变长对象,那么要调整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;
}
}
//dict不为NULL,从字典中获取
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;
}
}
}
}
//我们看到这里又判断了一次,但是这次少了个条件
//没错熟悉Python描述符的应该知道,上面的需要满足是数据描述符
//这个是非数据描述符
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 高度相似。
//Objects/object.c
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中得到证实。
//object.c
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)
{
//那么显然要从instance对象自身的__dict__中寻找属性
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
//但如果dict为NULL,并且dictoffset说明继承自变长对象,那么要调整tp_dictoffset
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__
最初被创建的地方。
//object.c
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;
//老规矩,name必须是PyUnicodeObject对象
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) {
//这行代码就是PyObject_GenericGetAttr中根据dictoffset获取dict对象的那段代码
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中,我们会看到这三种魔法方法对应的操作。
//typeobject,c
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 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这个指令长什么样子吧。
case TARGET(CALL_METHOD): {
/* Designed to work in tamdem with LOAD_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(); /* POP the NULL. */
}
else {
res = call_function(tstate, &sp, oparg + 1, NULL);
stack_pointer = sp;
}
PUSH(res);
if (res == NULL)
goto error;
DISPATCH();
}
//为了对比,我们再把CALL_FUNCTION的源码贴出来
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中,隐藏着一个惊天大秘密。
PyTypeObject PyFunction_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"function",
sizeof(PyFunctionObject),
//...
//...
//注意注意注意注意注意注意注意,看下面这行
func_descr_get, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyFunctionObject, func_dict), /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
func_new, /* tp_new */
};
我们发现 tp_descr_get 被设置成了func_descr_get,这意味着我们得到的是一个描述符。另外由于 girl.__dict__
中没有f,那么 girl.f 的返回值将会被 descriptor 改变,也就是 func_descr_get(Girl.f, girl, Girl)
//funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
//如果是类获取函数, 那么这里的obj就是NULL, type就是类对象本身
//如果是实例获取函数, 那么这里的obj就是实例对象, type仍是类对象本身
//如果obj为空, 说明是类获取, 那么直接返回func本身, 也就是原来的函数
if (obj == Py_None || obj == NULL) {
Py_INCREF(func);
return func;
}
//如果是实例对象, 那么通过PyMethod_New将函数和实例绑定在一起, 得到一个PyMethodObject对象
return PyMethod_New(func, obj);
}
func_descr_get将Girl.f对应的PyFunctionObject进行了一番包装,所以通过PyMethod_New,Python虚拟机在PyFunctionObject的基础上创建一个新的对象PyMethodObject,那么这个PyMethodObject是什么呢?到PyMethod_New中一看,这个神秘的对象就现身了:
//classobjet.c
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
PyMethodObject *im; //PyMethodObject对象的指针
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--;
}
//不使用缓冲池,直接创建PyMethodObject对象
else {
im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
if (im == NULL)
return NULL;
}
im->im_weakreflist = NULL;
Py_INCREF(func);
//im_func指向PyFunctionObject对象
im->im_func = func;
Py_XINCREF(self);
//im_self指向实例对象
im->im_self = self;
im->vectorcall = method_vectorcall;
_PyObject_GC_TRACK(im);
return (PyObject *)im;
}
一切真相大白,原来那个神秘的对象就是PyMethodObject对象,看到free_list这样熟悉的字眼,我们就知道Python内部对PyMethodObject的实现和管理中使用缓冲池的技术。现在再来看看这个PyMethodObject:
//classobject.h
typedef struct {
PyObject_HEAD
//可调用的PyFunctionObject对象
PyObject *im_func;
//self参数,instance对象
PyObject *im_self;
//弱引用列表
PyObject *im_weakreflist; /* List of weak references */
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,所以为什么实例调用方法的时候会自动传递第一个参数现在是真相大白了。
这个过程称之为成员函数的绑定,就是将实例和函数绑定起来,使之成为一个整体(方法)
。
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) # <function Girl.f at 0x000001B7805A2820>
print(girl.f) # <bound method Girl.f of <__main__.Girl object at 0x000001B7E92282B0>>
print(type(Girl.f)) # <class 'function'>
print(type(girl.f)) # <class 'method'>
我们看到通过类来调用成员的函数得到的就是一个普通的函数,如果是实例调用成员函数,那么会将成员函数包装成一个方法,也就是将成员函数和实例绑定在一起,得到结果就是方法,实现方式是通过描述符。
方法调用
在LOAD_METHOD指令结束之后,那么便开始了CALL_METHOD,我们知道这个和CALL_FUNCTION之间最大的区别就是,CALL_METHOD调用的是一个PyMethodObject对象,而CALL_FUNCTION调用的一个PyFunctionObject对象。
CALL_METHOD底层也调用了CALL_FUNCTION,因为方法是将函数和实例绑定在了一起,但最终执行的还是函数。
//ceval.c
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; //返回值
//如果func是一个函数, 那么直接通过_PyObject_Vectorcall进行调用, 然后将返回值设置给x
if (PyCFunction_Check(func)) {
C_TRACE(x, _PyObject_Vectorcall(func, args, nargs, kwnames));
return x;
}
//如果func是一个描述符, 注意:此时的 func 不是方法,方法的类型是 PyMethod_Type,这里是描述符
//那么nargs(func的参数个数)必须大于0, 因为默认会传递一个self
else if (Py_TYPE(func) == &PyMethodDescr_Type && nargs > 0) {
/* We need to create a temporary bound method as argument
for profiling.
If nargs == 0, then this cannot work because we have no
"self". In any case, the call itself would raise
TypeError (foo needs an argument), so we just skip
profiling. */
PyObject *self = args[0]; //self就是args的第一个参数
//通过调用 PyMethodDescr_Type 的tp_descr_get, 接收三个参数: 函数(显然是通过类获取的)、实例对象、类对象
//然后调用该描述符的 __get__ 方法,获取返回值
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变成一个方法。
//funcobject.c
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演示一下:
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参数。
class Girl(object):
def f(self):
print(self)
girl = Girl()
Girl.f(123) # 123
# 我们看到即便传入一个123也是可以的
# 这是我们自己传递的,传递什么就是什么
girl.f() # <__main__.A object at 0x000001F0FFE81F10>
# 但是girl.f()就不一样了,首先girl.f()表示先通过girl获取f对应值, 压入运行时栈, 然后再进行调用、完事之后将返回值设置在栈顶
# 而在girl.f的时候就已经通过func_descr_get(Girl.f, girl, Girl)将这个函数和调用者绑定在一起了
# 然后调用的时候自动将调用者作为第一个参数传递进去
print(Girl.f) # <function A.f at 0x000001F0FFEEFF70>
print(girl.f) # <bound method A.f of <__main__.A object at 0x000001F0FFE81F10>>
注意:我们上面一直说的是调用者(其实说调用者也不是很准确)
,而不是实例对象,这是因为函数不仅可以和实例绑定,也可以和类绑定。
class Girl(object):
@classmethod
def f(self):
print(self)
print(Girl.f) # <bound method Girl.f of <class '__main__.Girl'>>
print(Girl().f) # <bound method Girl.f of <class '__main__.Girl'>>
Girl.f() # <class '__main__.Girl'>
Girl().f() # <class '__main__.Girl'>
我们看到此时通过类去调用得到的不再是一个函数,而是一个方法,这是因为我们加上classmethod装饰器,当然classmethod也是一个描述符。当类在调用的时候,类也和函数绑定起来了,因此也会得到一个方法。不过被classmethod装饰之后,即使是实例调用,第一个参数传递的还是类本身,因为和 PyFunctionObject 绑定的是类、而不是实例。
所以得到的究竟是函数还是方法,就看这个函数有没有和某个对象进行绑定,只要绑定了,那么它就会变成方法。
千变万化的descriptor
当我们调用instance对象的函数时,最关键的一个动作就是从PyFunctionObject对象向PyMethodObject对象的转变,而这个关键的转变就取决于Python中的descriptor。当我们访问对象中的属性时,由于descriptor的存在,这种转换自然而然的就发生了。事实上,Python中的descriptor很强大,我们可以使用它做很多事情,而在Python的内部,也存在各种各样的descriptor,比如property、staticmethod、classmethod等等,这些descriptor给python的类机制赋予了强大的力量。具体源码就不分析了,我们直接通过Python代码的层面演示一下,这三种描述符的实现。
实现property
class Property:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
if instance is None:
# 如果instance是None说明是类调用,那么直接返回这个描述符本身
# 这个和内置property的处理方式是一样
return self
res = self.func(instance)
return res
class A:
@Property
def f(self):
return "name: hanser"
a = A()
print(a.f) # name: hanser
print(A.f) # <__main__.Property object at 0x000001FABFE910A0>
总结:property是为了实例对象准备的,当然property支持的功能远不止我们上面演示的这么简单,它还可以进行set、delete,这些我们在介绍魔法方法的时候再说吧。
实现staticmethod
class StaticMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
# 静态方法的话,类和实例都可以用
# 类调用不会自动传参,但是实例会自动传递,因此我们需要把实例调用传来的self给扔掉
# 做法是直接返回self.func即可,注意:self.func是A.func
# 因此调用的时候,是类去调用的,而类调用是不会自动加上参数的。
return self.func
class A:
@StaticMethod
def f():
return "name: hanser"
a = A()
print(a.f()) # name: hanser
print(A.f()) # name: hanser
总结:staticmethod也是为了实例对象准备的,但是类也可以调用。
实现classmethod
class ClassMethod:
def __init__(self, func):
self.func = func
def __get__(self, instance, owner):
# 类方法,目的是在类调用的时候,将类本身作为第一个参数传递进去
# 显然是这里的owner
# 返回一个闭包,然后当调用的时候,接收参数
# 不管是谁调用,最终这个self.func都是A.func,然后手动将cls也就是owner传递进去
def inner(*args, **kwargs):
return self.func(owner, *args, **kwargs)
# 所以在上面我们看到, 函数被classmethod装饰之后,即使是实例调用,第一个参数传递的还是类本身
return inner
class A:
name = "hanser"
@ClassMethod
def f(cls):
return f"name: {cls.name}"
a = A()
print(a.f()) # name: hanser
print(A.f()) # name: hanser
总结:classmethod是为了类对象准备的,但是实例也可以调用。
小结
这一次我们介绍了Python中实例对象的创建以及属性访问,下一篇我们介绍魔法方法。
如果觉得文章对您有所帮助,可以请囊中羞涩的作者喝杯柠檬水,万分感谢,愿每一个来到这里的人都生活愉快,幸福美满。
微信赞赏
支付宝赞赏