欢迎关注微信公众号——Python与统计分析,一起学习,一起交流。
相信所有学过Python的人都听过这样一句话:Python中,一切皆对象。一个整数是一个对象,产生这个整数的类int
也是一个对象。函数是一个对象,函数所在的模块也是一个对象。最让我们惊讶的是,就连代码本身也是对象。
Python把面向对象的思想发挥的淋漓尽致,然而对于初学者来说,我们还是不清楚什么是对象。Python官方解释器是由C语言编写的,解答这个问题需要从C语言中寻找答案。因此本文,我们主要从C的层面来认识对象。
对象机制的核心——PyObject
对于人来说,对象是一个比较具体的概念,比如说一个人,一张书桌。而对于计算机而言,对象却是一个抽象的概念。通常的说法是,对象是数据以及操作这些数据的方法的集合。由于数据和操作数据的方法都存储在计算机内存中,你可以理解一个对象就是计算机中存储这些数据和方法的一块内存空间,而这块内存可能是连续的,也可能是不连续的。如果是不连续的,我们需要从整体上来把握。再具体一些,从C的层面来看,Python的对象就是为C中的结构体在内存堆空间中申请的一块内存。
Python中有很多种类型的对象,不同类型的对象之间存在一些共性,这些共性定义在PyObject结构体中。
从Python官网下载Python解释器的源码包,解压打开Includeobject.h,我们就可以看到PyObject了,PyObject的定义如下:
//Include/object.h
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
可以看到PyObject
内部定义了一个_PyObject_HEAD_EXTRA
、一个ob_refcnt
和一个ob_type
。_PyObject_HEAD_EXTRA
是一个宏,这个宏主要使用来维护一个“双向链表”的,通常不启用,可以不用管。我们的关注点放在ob_refcnt
和ob_type
。
ob_refcnt
:即reference count,即引用计数。ob_type
:类型指针。
引用计数表示该对象被变量引用的次数,对象被引用1次,ob_refcnt就会加1,引用解除时,ob_refcnt就会减少1。引用计数是Python实现对象回收的重要机制之一。
我们注意到ob_type指向了一个_typeobject
的结构体,这个结构体是做什么的呢?我们说对象有三个重要特征:id、类型和值。_typeobject
就是用来指定一个对象的类型的。这部分内容我们后面再进行分析。
现在我们看到了,Python中对象至少包含两部分内容,一个是引用计数,一个是类型,这些是对象的共性,然而还有一些性质虽然不是所有对象所共有,但是是大部分对象所共有的,接下来我们就会看到。
定长对象与变长对象
Python中根据对象所占用的内存空间大小是否固定,可以将对象分为定长对象和变长对象。定长对象如整数。变长对象如字符串、列表等。字符串、列表中有多少个元素,都无法事先确定,只能使用变长对象来进行存储,变长对象在C层面对应于PyVarObject结构体。
同样可以在object.h中找到PyVarObject结构体:
//Include/object.h
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
可以看到,PyVarObject是对PyObject的扩展,在其基础上增加一个ob_size的字段。ob_size指明了变长对象中容纳了多少个元素。比如对于一个list,它就是一个变长对象,list中有5个元素,ob_size就等于5。
Python中的对象都是在PyVarObject或者PyObject基础上进行的扩展,例如:float实例对象,其结构体定义为:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
可见,float实例对象在内存中对应PyFloatObject结构体,其是在PyObject基础上增加了一个双精度的ob_fval,其代表的就是该实例对象的值。
再如list实例对象,其结构体的定义为:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
list实例对象是一个PyListObject的结构体,是在PyVarObject的基础之上增加了两个字段:
- ob_item: 指向 动态数组 的指针,数组保存元素对象指针;
- allocated: 动态数组总长度,即列表当前的 容量
float实例对象和list实例对象在介绍相应数据类型的时候再进行阐述。
类对象
除了实例化对象之外,Python中类也是一种对象,我们称之为类对象。类对象之间也有一些共性,接下来我们看就来看一下。
首先我们要明确,类对象作为一种对象,其基本结构应该是在PyObject基础上扩展的,具体而言Python中的类对象是以PyTypeObject结构体为基础的,现在我们请出PyTypeObject的结构体,看看其真身如何。
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name;
Py_ssize_t tp_basicsize, tp_itemsize;
destructor tp_dealloc;
printfunc tp_print;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
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;
PyBufferProcs *tp_as_buffer;
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
traverseproc tp_traverse;
inquiry tp_clear;
richcmpfunc tp_richcompare;
Py_ssize_t tp_weaklistoffset;
getiterfunc tp_iter;
iternextfunc tp_iternext;
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
unsigned int tp_version_tag;
destructor tp_finalize;
#ifdef COUNT_ALLOCS
Py_ssize_t tp_allocs;
Py_ssize_t tp_frees;
Py_ssize_t tp_maxalloc;
struct _typeobject *tp_prev;
struct _typeobject *tp_next;
#endif
} PyTypeObject;
#endif
PyTypeObject中的字段非常多,我们首先看到的是PyObject_VAR_HEAD
,这是一个宏,其实就是提示PyTypeObject是一个变长对象。其他一些关键字段包括:
tp_name
:类对象的名称,是一个char 。tp_basicsize, tp_itemsize
:创建对应实例对象时所需要的内存信息。tp_dealloc
:其实例对象执行析构函数时所作的操作。tp_print
:其实例对象被打印时所作的操作。tp_base
:继承的基类。
PyTypeObject结构体初始化设置不同的值,就会得到Python中不同的内置类对象,例如float
类对象的结构体如下:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)float_repr, /* tp_repr */
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)float_hash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
float_new__doc__, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
float_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
float_methods, /* tp_methods */
0, /* tp_members */
float_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
float_new, /* tp_new */
};
float类对象是一个PyFloat_Type的结构体。第二行是初始化PyTypeObject的 ob_refcnt 、 ob_type 以及 ob_size 三个字段 ,第三行是初始化PyTypeObject的tp_name字段为float,......,这样Python内置的类对象float就诞生了。
现在我们应该清楚了,一个PyTypeObject对象就是面向对象理论中类对象的实现,我们熟知的int
类、float
类、list
类都是一个PyTypeObject结构体,都是在这个结构体基础上进行了一些初始化。从PyTypeObject结构体中我们可以推测,其中存储了很多实例化对象的信息,这些信息在后面我们再详细介绍。
有了上面的基础,下面我们再来看一下Python中两个特殊的对象:type对象和object对象。
Type对象
type到底是一个什么对象呢?我们说类也是一个对象,既然类是一个对象,在计算机底层必然至少存在一个引用计数,一个类型指针,而这个类型指针指向的就是这个类对象的类型对象。类的类型对象是什么呢?先别急,我们来作一个实验:
In [1]: float.__class__
Out[1]: type
In [2]: int.__class__
Out[2]: type
In [3]: list.__class__
Out[3]: type
Python中打印一个对象的__class__
属性就会返回这个对象的类型对象。我们看到,float类对象、int类对象、list类对象的__class__
属性返回值都是type
,也就是说float类对象、int类对象、list类对象的类型对象都是type对象。实际上,Python中所有内置类对象的类型对象都是type对象。那么type对象在计算机中长什么样子呢?
// Objects/typeobject.c
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
(destructor)type_dealloc, /* tp_dealloc */
offsetof(PyTypeObject, tp_vectorcall), /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)type_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
(ternaryfunc)type_call, /* tp_call */
0, /* tp_str */
(getattrofunc)type_getattro, /* tp_getattro */
(setattrofunc)type_setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS |
Py_TPFLAGS_HAVE_VECTORCALL, /* tp_flags */
type_doc, /* tp_doc */
(traverseproc)type_traverse, /* tp_traverse */
(inquiry)type_clear, /* tp_clear */
0, /* tp_richcompare */
offsetof(PyTypeObject, tp_weaklist), /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
type_methods, /* tp_methods */
type_members, /* tp_members */
type_getsets, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyTypeObject, tp_dict), /* tp_dictoffset */
type_init, /* tp_init */
0, /* tp_alloc */
type_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
(inquiry)type_is_gc, /* tp_is_gc */
};
很显然,type对象与类对象float、int等一样,也是一种PyTypeObject的结构体实例,这个结构体实例名称为PyType_Type,只不过在初始化赋值时,接收的是和float、int不同的参数。比如说在第三行,PyTypeObject的tp_name字段接收一个"type"的值,这表明这个PyTypeObject结构体实例是一个被称为type的对象。第十二行,tp_as_number字段接收一个0的值,这表明type对象并未定义数学运算相关方法。其他字段在我们后续的推送中会逐一进行讲解,现在我们需要考虑一个问题,type是一个PyTypeObject结构体实例,是一个对象,那么它的类型对象是什么?
这个秘密隐藏在PyType_Type结构体实例的第二行中。我们注意到第二行PyVarObject_HEAD_INIT是一个带参数的宏,这个宏在CPython中的定义如下:
// Include/object.h
#define PyObject_HEAD_INIT(type)
{ _PyObject_EXTRA_INIT
1, type },
#define PyVarObject_HEAD_INIT(type, size)
{ PyObject_HEAD_INIT(type) size },
宏PyVarObject_HEAD_INIT中包含了另外一个宏PyObject_HEAD_INIT。而后者接收一个type的参数,用来初始化PyObject。具体而言,当创建一个PyObject时,调用这个宏,可以将对象的引用计数ob_refcnt字段初始化为1,将ob_type初始化为type。以此类推,PyVarObject_HEAD_INIT宏是用来初始化一个PyVarObject的,其增加了一个参数,用来指定PyVarObject的长度,即初始化ob_size字段。
有了上面的介绍,我们再来看一下PyType_Type结构体实例的第二行。我们看到,PyType_Type的宏PyVarObject_HEAD_INIT接收的两个参数分别为&PyType_Type和0,这表明Python解释器将type对象的ob_type字段初始化为PyType_Type,而将ob_size字段出视化为0。
什么?type对象的ob_type字段初始化为PyType_Type?这不是意味着type对象的类型对象是自己吗?确实如此,且看如下演示:
In [4]: type.__class__
Out[4]: type
现在我们应该对type对象有了一定认识了,Python中内置类对象的类型对象都是type对象,而type对象的类型对象是本身。由于type对象是类对象的类型对象,通常把type对象称之为元类(metaclass)。
Object对象
说完了type元类对象,我们再来说说object对象。我们在学习Python编程时,我们说用class语句自定义一个类,如:
In [5]: class A:
...: pass
...:
那么,类A默认会继承自一个object类。而且对于一个类,我们打印其__base__
属性,就会返回这个类所继承的父类:
In [6]: A.__base__
Out[6]: object
结果符合我们的预期。那么object又是什么样子呢?
// Objects/typeobject.c
PyTypeObject PyBaseObject_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"object", /* tp_name */
sizeof(PyObject), /* tp_basicsize */
0, /* tp_itemsize */
object_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
object_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)_Py_HashPointer, /* tp_hash */
0, /* tp_call */
object_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
object_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
object_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
object_methods, /* tp_methods */
0, /* tp_members */
object_getsets, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
object_init, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
object_new, /* tp_new */
PyObject_Del, /* tp_free */
};
object显然也是一个以PyTypeObject结构体为基础的类对象,这个结构体名字为PyBaseObject_Type。当PyTypeObject结构体在初始化为PyBaseObject_Type时,我们注意到tp_name字段被初始化为'object',ob_type字段被初始化为PyType_Type。因此,我们在解释器中输入如下命令,返回object对象的类型对象为type对象。
In [7]: object.__class__
Out[7]: type
我们再来介绍一个字段tp_base,PyTypeObject结构体初始化时,tp_base字段用来设置对象所继承的父类,我们发现这里的tp_base字段初始化设置为0,这表明object的父类为空。Python解释器提供了__base__
属性可以查看一个类对象所继承的父类:
In [8]: print(object.__base__)
None
结果是符合预期的。那么为什么要将object对象的父类设置为空呢?因为继承链必须有一个终点,object被Python解释器设定为“对象之父”,也就是说一切对象都继承自object,那么继承链到达object这里必须要停止,否则就会陷入无穷的循环之中。
学习了一个新的字段tp_base,相信你一定会回头看看之前介绍的type元类对象和float类对象对应结构体实例的tp_base 字段设置吧。我们检查发现,这两个字段都被设置为了0。怎么回事?这两个字段不应都被设置为object吗?
In [11]: type.__base__
Out[11]: object
In [12]: float.__base__
Out[12]: object
实际上,你会发现所有内置类对象的tp_base字段都被初始化为0。为0的原因在于目前我们看到的类对象都是一个静态的半成品。Python是一个动态语言,不可能在定义的时候将所有字段都设置好,然后在解释器启动时就得到我们使用的类型对象。实际上这些类对象对应结构体在解释器启动后,还会再进行一次动态的完善,届时就会得到我们最终使用的类对象。
至于怎么完善,以及那些字段会完善,我们还是放在后面再进行讲解。
总结
至此,我们比较系统的介绍了Python中的主要对象及其在计算机中的表现形式。接下来我们通过一个例子,以图示的形式来完整介绍一下实例对象、类对象、type元类对象、object类对象之间的关系。如图所示:
In [1]: e = 2.71828
In [2]: e.__class__
Out[2]: float
In [3]: float.__class__
Out[3]: type
In [4]: float.__base__
Out[4]: object
In [5]: type.__class__
Out[5]: type
In [6]: type.__base__
Out[6]: object
In [7]: object.__class__
Out[7]: type
In [8]: object.__base__
In [9]: e.__base__
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<iPython-input-9-e38e26eb69ec> in <module>
----> 1 e.__base__
AttributeError: 'float' object has no attribute '__base__'
![d0d650b7c8f6df7a1fe76b6ee16935e8.png](https://i-blog.csdnimg.cn/blog_migrate/9cfca1b90cb2ec69a5cfbbc5ba8f7c31.jpeg)
- 在Python解释器中输入的e、float、type、object都仅仅是一个名字,这些名字分别指向内存中的四个结构体,分别为
PyFloatObject、PyFloatType、PyType_Type、PyBaseObject_Type
。 - 这些结构体都有的部分是PyObject,包含一个引用计数,一个类型指针。
- 所有对象都有类型对象,e实例的类型指针指向了float类对象,表明e是一个float类型。同样float类对象和object类对象的类型指针都指向了type元类对象,表明float类对象和object类对象的类型对象为type。此外,type元类对象的类型对象是自身。
- 类对象包括一个ob_base字段,表明类对象都有基类。float类对象和type类对象的基类都是object类,而object类的基类为空。
上图较详细的呈现了Python中对象之间的关系,我们将这些关系称之为对象模型。对象模型将是理解Python运行机制的核心,大家应该牢牢掌握。
参考资料
[1] 陈儒.Python源码剖析——深度探索动态语言核心技术[M].电子工业出版社:北京,2008:1-497
[2] fasionchan.Python 源码深度剖析[EB/OL]. https://www. imooc.com/read/76,2020 .
[3] Python.Python对象实现支持[EB/OL]. https:// docs.python.org/zh-cn/3 /c-api/objimpl.html
[4] Obi lke-Nwosu.Inside the Python Virtual Machine[M].Leanpub, 2018:1-126