1. 对象的创建
首先我们需要观察思考:
- Python 内部是如何从无到有创建一个浮点对象的
- Python 又是怎么知道该如何将它打印到屏幕上的呢?
>>> pi = 3.14
>>> print(pi)
3.14
下面以 floatl类型为例子,对应C实体是 PyFloat_Type
-
首先来介绍C API
-
Python 是用 C 写成的,对外提供了 C API ,让用户可以从 C 环境中与其交互。 Python 内部也大量使用这些 API ,为了更好研读源码,先系统了解 API 组成结构很有必要。 C API 分为两类: 泛型 API 以及 特型 API 。
- (1)泛型 API
- 泛型 API 与类型无关,属于 抽象对象层 ( Abstract Object Layer ),简称 AOL 。 这类 API 参数是 PyObject* ,可处理任意类型的对象, API 内部根据对象类型区别处理
- 我的理解就是通用的接口, 就像人一样,每个人都需要 吃饭,睡觉。 吃饭,睡觉,就是泛型API,处理任意类型的对象(人),所谓抽象,就是不是具体指某个实际对象,而是抽提出来的公共部分,所称为 抽象。(看过java基础,对抽象类概念的理解)
- 以对象打印函数为例子:
- (1)泛型 API
int
PyObject_Print(PyObject *op, FILE *fp, int flags)
接口第一个参数为待打印对象,可以是任意类型的对象,因此参数类型是 PyObject* 。 Python 内部一般都是通过 PyObject* 引用对象,以达到泛型化的目的
* 对于任意类型的对象,均可调用 PyObject_Print 将其打印出来(抽象)
// 打印浮点对象
PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Print(fo, stdout, 0);
// 打印整数对象
PyObject *lo = PyFloatObject_FromLong(100);
PyObject_Print(lo, stdout, 0);
PyObject_Print 接口内部根据对象类型,决定如何输出对象。
- (2)特型 API
- 特型 API 与类型相关,属于 具体对象层 ( Concrete Object Layer ),简称 COL 。 这类 API 只能作用于某种类型的对象,例如浮点对象 PyFloatObject 。 Python 内部为每一种内置对象提供了这样一组 API
- 很容易理解,就是针对 某个类型,提供特定的方法接口。
例如:
PyObject *
PyFloat_FromDouble(double fval)
PyFloat_FromDouble 创建一个浮点对象,并将它初始化为给定值 fval 。
对象的创建
到这里我们终于要讲对象的创建了! 当然前面的介绍是少不了的
- 经过上面一节的学习,我们知道了 元数据的概念,它保存在类型对象中。
- 1.元数据 保存着创建对象的 内存等
在这里插入代码片
信息。 - 2.支持的操作 。
- 3 也包括对象如何创建的信息。
- 1.元数据 保存着创建对象的 内存等
- 实际上,不管创建爱你对象的流程如何,最终的关键步骤都是 分配内存。
总结起来python内部通过两种方式创建对象
- (1)通过 C API ,例如 PyFloat_FromDouble ,多用于内建类型;比如 上面提到的 float
- (2) 通过类型对象,例如 Dog ,多用于自定义类型
- 通过类型对象创建实例对象,是一个更通用的流程(大部分创建多使用这个方法),同时支持内置类型和自定义类型。 以创建浮点对象为例,我们还可以通过浮点类型 PyFloat_Type 来创建:例如
>>> pi = float('3.14')
>>> pi
3.14
2.对象的调用及多态性
(1)可调用对象:
- 上面通过调用 类型对象的方式创出来的一个实例 pi, 这就说明对象是可调用的
- 对象被调用时,执行的函数肯定是定义在了类型当中,那里保存着实例对象(类型对象之前所过,也是被type实例的 实例对象)的元信息
- 所以再type中(PyType_Type):找到一个字段:tp_call
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
// ...
(ternaryfunc)type_call, /* tp_call */
// ...
};
我们知道了,当实例对象被调用时,便执行 tp_call 字段保存的处理函数。!这就是 python对象调用当中,底层C中的实现!!!
- 因此 float(‘3.14’) 在 C 层面等价于:
PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)
1. 首先浮点类型对象 ,调用ob_type字段对应的 类型: PyType_Type
2. 调用其中的tp_call, 完成对象的调用
即:
PyType_Type.tp_call(&PyFloat_Type, args, kwargs)
最终执行, type_call 函数:
type_call(&PyFloat_Type, args, kwargs)
书作者:调用参数通过 args 和 kwargs 两个对象传递,先不展开,留到函数机制中详细介绍。
- 下面我们开始看type_call 函数,定义于 Include/typeobject.c ,关键代码如下
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
PyObject *obj;
// ...
obj = type->tp_new(type, args, kwds);
obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);
if (obj == NULL)
return NULL;
// ...
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;
}
- 不懂C语言没关系,只要能看懂大概:
*(1)调用类型对象 tp_new 函数指针 申请内存 (第 7 行);- (2)必要时调用类型对象 tp_init 函数指针对对象进行 初始化 (第 15 行)
到这里对象的创建过程已经十分清晰了!
总结一下:
- 调用 float (这里类型对象创建实例), Python 最终执行其类型对象 type 的 tp_call 函数;
- tp_call 函数调用 float 的 tp_new 函数为实例对象分配 内存空间
- tp_call 函数必要时进一步调用 tp_init 函数对实例对象进行 初始化
(2)对象的多态性实现
- 首先,要实现多态性,首先要明白什么是多态!我找到一篇文章,里面例子介绍什么是多态,希望 认真观看:https://baijiahao.baidu.com/s?id=1646997242602110678&wfr=spider&for=pc
- 面相对象程序设计的三大特性:封装性,继承性,多态性!
- 我的理解,多态性,先从这个词理解就是:多种形态, 指的就是一个类型的属性或方法在子类中表现为不同的形态,既多种形态。就像上面的链接中的例子:多态性的应用实例, 我们三次执行时,调用的都是父类的同一个方法:construct(), 而传入的参数不同,父类会调用不同的 子类方法实现。
那么内部C语言层面上具体是如何进行实现多态性的呢?
- 类似于上面所说的,Python 创建一个对象,比如 PyFloatObject ,会分配内存,并进行初始化。 此后, Python 内部统一通过一个 PyObject* 变量来保存和维护这个对象,而不是通过 PyFloatObject* 变量
- 通过PyObject* 变量保存和维护对象,可以实现更抽象的上层逻辑,而不用关心对象的实际类型和实现细节。也就是泛型。抽象的,任何实际类型都可以传入,并完成对应的保存和维护。
- 文中提到:以对象哈希值计算为例,假设有这样一个函数接口:
Py_hash_t
PyObject_Hash(PyObject *v);
// 该函数可以计算任意对象的哈希值,不管对象类型是啥。 例如,计算浮点对象哈希值:
PyObject *fo = PyFloatObject_FromDouble(3.14);
PyObject_Hash(fo);
// 对于其他类型,例如整数对象,也是一样的:
PyObject *lo = PyLongObject_FromLong(100);
PyObject_Hash(lo);
- 可以看到上面的例子,一个函数,可以解决所有类型的对象求hash值的操作,答案在Object/object.c中
Py_hash_t
PyObject_Hash(PyObject *v)
{
PyTypeObject *tp = Py_TYPE(v);
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
/* To keep to the general practice that inheriting
* solely from object in C code should work without
* an explicit call to PyType_Ready, we implicitly call
* PyType_Ready here and then check the tp_hash slot again
*/
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
return -1;
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
}
/* Otherwise, the object can't be hashed */
return PyObject_HashNotImplemented(v);
}
- 首先函数通过ob_type 指针找到对象的类型 (第 4 行)
- 然后通过类型对象的 tp_hash 函数指针,调用对应的哈希值计算函数 (第 6 行)。
- 也就是说: PyObject_Hash 根据对象的类型,调用不同的函数版本!! 这不就是多态吗?! 类似于上面链接中讲的例子,根据传入的实例对象不同,而调用对象的相对应方法完成操作! 这就是通过 ob_type 字段, Python 在 C 语言层面实现了对象的 多态 特性!
对象的行为
- 我们上面的一节 通过提出两个问题说过,对象的 元数据保存在 类型对象中,并且规定了对象的 行为,既操作。
- 不同对象的行为不同,比如哈希值计算方法就不同,由类型对象中 tp_hash 字段决定。 除了 tp_hash ,我们看到 PyTypeObject 结构体还定义了很多函数指针,这些指针最终都会指向某个函数,或者为空。 这些函数指针可以看做是 类型对象 中定义的 操作 ,这些操作决定对应 实例对象 在运行时的 行为 。
- 然而,除了对象特有的行为,不同对象还会有一些共性:比如 整数对象 和 浮点对象 都支持加减乘除等 数值型操作, 元组对象 tuple 和 列表对象 list 都支持下标索引操作,
>>> 1 + 2
3
>>> 3.14 * 3.14
9.8596
-
因此根据对象的行为,将对象进行分类:
(这也是很好的理解了,python中的对象分类!,受益匪浅,大彻大悟~) -
Python 便以此为依据,为每个类别都定义了一个 标准操作集 :
- PyNumberMethods 结构体定义了 数值型 操作
- PySequenceMethods 结构体定义了 序列型 操作;
- PyMappingMethods 结构体定义了 关联型 操作
-
只要 类型对象 提供相关 操作集 , 实例对象 便具备对应的 行为!
-
这里截取书中部分代码,展示例子:
// 以 float 为例,类型对象 PyFloat_Type 相关字段是这样初始化的:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
// ...
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
// ...
};
- 解析:
- 字段 tp_as_number 非空,因此 float 对象 支持数值型操作 ;
- 字段 tp_as_sequence 为空,因此 float 对象 不支持序列型操作 ;
- 字段 tp_as_mapping 为空,因此 float 对象 不支持关联型操作 ;
引用计数
原话引入:
简单的例子(均引用书中例子):
>>> a = 3.14
>>> sys.getrefcount(a)
2
# 这里注意:对象作为函数参数传递,需要将引用计数加一,避免对象被提前销毁;
# 函数返回时,再将引用计数减一。 因此,例子中 getrefcount 函数看到的对象引用计数为 2 !!
# 接着,变量赋值让对象多了一个引用,这很好理解:
>>> b = a
>>> sys.getrefcount(a)
3
# 将对象放在容器对象中,引用计数也增加了,符合预期:
>>> l = [a]
>>> l
[3.14]
>>> sys.getrefcount(a)
4
# 我们将 b 变量删除,引用计数减少了:
>>> del b
>>> sys.getrefcount(a)
3
# 接着,将列表清空,引用计数进一步下降:
>>> l.clear()
>>> sys.getrefcount(a)
2
# 最后,将变量 a 删除后,引用计数降为 0 ,便不复存在了:
>>> del a
- python中很多场景涉及到 应用计数的调整:
- 容器操作
- 变量赋值
- 函数参数传递
- 属性操作
- 为此,Python 定义了两个非常重要的宏,用于维护对象应用计数。 其中, Py_INCREF 将对象应用计数加一 ( 3 行):
#define Py_INCREF(op) ( \
_Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \
((PyObject *)(op))->ob_refcnt++)
- Py_DECREF 将引用计数减一 ( 5 行),并在引用计数为 0 时回收对象 ( 8 行):
#define Py_DECREF(op) \
do { \
PyObject *_py_decref_tmp = (PyObject *)(op); \
if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \
--(_py_decref_tmp)->ob_refcnt != 0) \
_Py_CHECK_REFCNT(_py_decref_tmp) \
else \
_Py_Dealloc(_py_decref_tmp); \
} while (0)
最后:(作者话语)
当一个对象引用计数为 0 , Python 便调用对象对应的析构函数销毁对象,但这并不意味着对象内存一定会回收。 为了提高内存分配效率, Python 为一些常用对象维护了内存池, 对象回收后内存进入内存池中,以便下次使用,由此 避免频繁申请、释放内存 。
内存池 技术作为程序开发的高级话题,需要更大的篇幅,放在后续章节中介绍。 让我们期待吧!,我也会持续学习,总结