众所周知,Python是一门面向对象的语言,在Python无论是数值、字符串、函数亦或是类型、类,都是对象。
对象是在堆
上分配的结构,我们定义的所有变量、函数等,都存储于堆内存,而变量名、函数名则是一个存储于栈
中、指向堆中具体结构的引用。
- 堆:主要负责存储Python运行时的所有对象实体(也就是Python对象的所有属性数据),例如在执行
s='Hello Word'
这条语句时,Python会为'Hello Word'
这个字符串创建一个PyASCIIObject对象实体
,并将其存储到堆
内存中。 - 栈:又叫数据栈或值栈,它主要负责保存对堆中Python对象的引用,例如在执行
s='Hello Word'
这条语句时,Python会将'Hello Word'这个字符串对象实体所处的内存地址
压入栈
,而不是将'Hello Word'这个字符串值压入栈。
堆和栈
要想深入学习Python,首先需要知道Python对象的定义。
Python对象定义
Python对象PyObejct
我们通常说的Python都是指CPython,底层由C语言实现,源码地址:cpython [GitHub]
Python对象的定义位于Include/object.h
,是一个名为PyObject
的结构体:
/* 定义指针以支持所有活动堆对象的双链表 */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
/* 实际上没有任何东西被声明为PyObject,但每个指向Python对象的指针都可以转换为PyObject*。这是手动建立的继承。
* 同样,每个指向可变大小Python对象的指针也可以转换为PyVarObject*。
*/
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt; // 引用计数
PyTypeObject *ob_type; // 类型对象指针,定义Python对象类型
} PyObject;
Python中的所有对象都继承自PyObejct,PyObject包含一个用于垃圾回收的双向链表,一个引用计数变量ob_refcnt
和 一个类型对象指针ob_type
可变对象PyVarObject
从PyObejct的注释中,我们可以看到这样一句:每个指向可变大小Python对象
的指针也可以转换为PyVarObject*
(可变大小的Python对象会在下文中解释)。PyVarObejct
就是在PyObject的基础上多了一个ob_size
字段,用于存储元素个数:
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* 可变部分中的元素数量 */
} PyVarObject;
Python类型对象PyTypeObject
在PyObject结构中,还有一个类型对象指针ob_type
,用于表示Python对象是什么类型,定义Python对象类型的是一个PyTypeObject
接口体
typedef struct _typeobject PyTypeObject;
实际定义是位于Include/cpython/object.h
的_typeobject
:
struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name; /* 用于打印,格式为"<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* 用于分配大小 */
/* 实现标准操作的方法 */
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* Python 2中的tp_compare 或 Python 3中的tp_reserved */
reprfunc tp_repr;
/* 标准类的方法集 */
PyNumberMethods *tp_as_number; // 数值对象方法
PySequenceMethods *tp_as_sequence; // 序列对象方法
PyMappingMethods *tp_as_mapping; // 字典对象方法
/* 更多标准操作(此处用于二进制兼容性) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
...
};
在这个类型对象中,不仅包含了对象的类型,还包含了如分配内存大小、对象标准操作等信息,主要分为:
- tp_name:类型名,Python内部使用
- 创建该类型对象时分配的空间大小信息,即
tp_basicsize
和tp_itemsize
- 与该类型对象相关的操作信息
- 一些对象属性
以Python中的int类型
为例,int类型对象的定义如下:
PyTypeObject PyLong_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"int", /* tp_name */
offsetof(PyLongObject, ob_digit), /* tp_basicsize */
sizeof(digit), /* tp_itemsize */
...
};
Python类型的类型PyType_Type
从PyObject的定义中我们知道,每个对象的ob_type
都要指向一个具体的类型对象,比如一个数值型对象100
,它的ob_type会指向int类型对象PyLong_Type
。
PyTypeObject结构体第一行是一个PyObject_VAR_HEAD宏,查看宏定义可知PyTypeObject是一个变长对象
/ * PyObject VAR HEAD定义了所有大小可变的容器对象的初始段。
* 最后声明了一个只有1个元素的数组,但是已经分配了足够的空间,使得数组实际上有足够的空间容纳ob_size的元素。
* 注意,ob size是一个元素计数,不一定是字节计数。
*/
#define PyObject_VAR_HEAD PyVarObject ob_base;
也就是说,归根结底类型对象也是一个对象
,也有ob_type属性,那PyLong_Type
的ob_type
是什么呢?
回到PyLong_Type的定义,第一行PyVarObject_HEAD_INIT(&PyType_Type, 0)
,查看对应的宏定义
// Include/object.h
#ifdef Py_TRACE_REFS
...
#define _PyObject_EXTRA_INIT 0, 0,
#else
...
# define _PyObject_EXTRA_INIT
#endif
#define PyObject_HEAD_INIT(type) \
{ _PyObject_EXTRA_INIT \
1, type },
#define PyVarObject_HEAD_INIT(type, size) \
{ PyObject_HEAD_INIT(type) size },
由以上关系可以知道,PyVarObject_HEAD_INIT(&PyType_Type, 0)
= { { _PyObject_EXTRA_INIT 1, &PyType_Type } 0}
,将其代入PyObject_VAR_HEAD
,得到一个变长对象:
/ * PyObject_VAR_HEAD,实际是PyVarObject */
{
/ * ob_base,即PyObject */
{
_PyObject_EXTRA_INIT // _PyObject_HEAD_EXTRA
1; // ob_refcnt=1
&PyType_Type; // ob_type=&PyType_Type
}
0; // ob_size=0
}
这样看就很明确了,PyLong_Type的类型就是PyType_Typ,同理可知,Python类型对象的类型就是PyType_Type
,而PyType_Type对象的类型是它本身
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
...
};
对象的类型关系
Python对象分类
从上述内容中,我们知道了对象和对象类型的定义,那么根据定义,对象可以有以下两种分类
按大小是否可变分类
Python对象定义有PyObject
和PyVarObject
,因此,根据对象大小是否可变的区别,Python对象可以划分为可变对象(变长对象)
和不可变对象(定长对象)
- 定长对象:变量发生改变时,对象大小是不变的,改变的是引用
比如,s = 'hello '
,变量s指向一个字符串(对象a),当s发生改变:s += ' world'
,变量s指向了一个新的字符串(对象b):
>>> s = 'hello '
>>> id(s)
17123904
>>> s += 'world'
>>> id(s)
8213552
原本的对象a大小并没有改变,只是s引用的对象改变了。这里的对象a、对象b就是定长对象
- 变长对象:变量发生改变时,变量的引用不变,改变的是对象的内容
比如,l = [1, 2, 3]
,变量l指向一个数组(对象a),当l发生改变:l.append(4)
,l += [5]
:
>>> l = [1, 2, 3]
>>> id(l)
7686744
>>> l.append(4)
>>> id(l)
7686744
>>> l += [5]
>>> id(l)
7686744
可以看到,变量l仍然指向对象a,只是对象a的内容发生了改变,数据量变大了。这里的对象a就是变长对象
由于存在以上特性,所以使用这两种对象还会带来一种区别:
声明s2 = s
,修改s的值:s = 'new string'
,s2的值不会一起改变,因为只是s指向了一个新的对象,s2指向的旧对象的值并没有发生改变
声明l2 = l
,修改l的值:l.append(6)
,此时l2的值会一起改变,因为l和l2指向的是同一个对象,而该对象的内容被l修改了
此外,对于字符串
对象,Python还有一套内存复用机制,如果两个字符串变量值相同,那它们将共用同一个对象:
>>> s1 = 'Hello'
>>> s2 = 'Hello'
>>> id(s1)
25512608
>>> id(s2)
25512608
>>>
对于数值型
对象,Python会默认创建0~28 以内的整数对象,也就是0 ~ 256
之间的数值对象是共用的:
>>> n1=0
>>> n2=0
>>> id(n1)
2038522960
>>> id(n2)
2038522960
>>>
>>> n1 = 256
>>> n2 = 256
>>> id(n1)
2038527056
>>> id(n2)
2038527056
>>>
>>> n1 = 257
>>> n2 = 257
>>> id(n1)
19586496
>>> id(n2)
25412160
按数据类型分类
按照Python数据类型,对象可分为以下几类:
- Fundamental 对象:类型对象
- Numberic 对象:数值对象
- Sequence 对象:容纳其他对象的序列集合对象
- Mapping 对象:键值对关联的字典对象
- Internal 对象:Python虚拟机内部使用的对象
对象的创建
Python创建对象有两种方式,泛型API和和类型相关的API
泛型API(Abstract Object Layer)
这类API通常以PyObject_xxx
的形式命名,可以应用在任意Python对象上,如:
- PyObject_New(type, typeobj) 为给定类型的对象分配内存,并初始化该对象的一部分
- PyObject_NewVar(type, typeobj, n) 为可变大小的对象分配内存并分配n个元素的空间
- PyObject_Init(op, typeobj) 不分配内存,接受指向新对象(由任意分配器分配)的指针,而不是“类型”形参,并初始化其对象头字段
- PyObject_InitVar(op, typeobj, n) 同上
使用PyObjecg_New
创建一个数值型对象:
PyObject* longObj = PyObject_New(PyObject, &PyLong_Type);
与类型相关API(Concrete Object Layer)
这类API通常只能作用于一种类型的对象上,如:
- PyObject * PyLong_FromLong(long ival) 通过长整型创建新对象
- PyObject * PyLong_FromUnsignedLong(unsigned long ival) 通过无符号长整型创建新对象
- ...
使用PyLong_FromLong
创建一个数值型对象:
PyObject* longObj = PyLong_FromLong(10)
对象的多态性
在我们使用Python声明变量的时候,并不需要为变量指派类型,在给变量赋值的时候,可以赋值任意类型数据,如:
a = 1
a = 'string'
a = [1, 2, 3]
从Python对象的定义我们已经可以知晓造成这个特点的原因了,Python创建对象时,会分配内存进行初始化,然后Python内部通过PyObject*
变量来维护这个对象,所以在Python内部各函数直接传递的都是一种泛型指针PyObject*
,这个指针所指向的对象类型是不固定的,只能通过所指对象的ob_type
属性动态进行判断,而Python正是通过ob_type
实现了多态机制
引用计数
Python在管理维护对象时,通过引用计数来判断内存中的对象是否需要被销毁,Python中所有事物都是对象,所有对象都有引用计数ob_refcnt
。
当一个对象的引用计数减少到0之后,Python将会释放该对象所占用的内存和系统资源。
但这并不意味着最终一定会释放内存空间,因为频繁申请释放内存会大大降低Python的执行效率,因此Python中采用了内存对象池的技术,是的对象释放的空间会还给内存池,而不是直接释放,后续需要申请空间时,优先从内存对象池中获取。
网友评论