深入理解python之对象系统

我们在学习理解一个事物时,往往遵循着由表及里的规律。第一步,我们学习一个事物的特性表现(feature)。在对事物的表现有了掌握以后,进一步的,我们尝试探索事物表现背后的内在原理。本文在叙述过程中,也遵循这样一个过程。第一部分是对python对象系统feature的简要概述,第二部分则是对python 对象系统底层实现的梳理与总结,在文章的最后一部分,是对常用的内置python类型实现的一个介绍。

Python 中的对象与类型的行为表现

我们在学习python的过程中,比较经常听到的一句话叫做 ,python处处皆对象。这是对于python对象模型的高度概括的描述。Python中的变量,实际上是一个名字,指向所引用的对象。Python中有“名字空间”的概念,这类似于其他编程语言中的作用域。在python中,名字空间底层由一个dict实现,变量名就是字典中的键,而变量引用的对象就是字典中键对应的值。

名字空间
名字空间

那么现在我们知道,不同于其他的语言,python中的整数,布尔值,字符串等类型的变量均是一个对象而不是简单的值。更进一步的,python中的所有语法元素,全部都是对象。函数是函数对象,模块是模块对象,最特殊的是,对象的类型也是一个对象

Python中的面向对象机制与java等语言,所有的类型默认继承自基类object,或者某个object的派生类型。但是由于在python中类型也是一个对象,所以包含基类object在内的所有类型,都是对象实例。区别于普通对象,我们姑且把描述类型的对象叫做 类对象。所有类对象的类型被叫做元类(metaclass),元类默认是一个type类对象,或者某个继承自type的类对象。最后,type类对象的类型是type自身。

类对象总结:

类对象 基类 类对象的类型
普通类型 object/object 派生类 Type/type 派生类
元类 type type
Object(默认基类) object type
Type(默认元类) object type

Python 底层对于对象与类型的实现

在对python语言层面上的对象与类型(对象)的行为表现有了了解以后,下面我们来研究一下,在python的底层是如何用c语言实现python对象与python类型的。

对象

python对象在python实现中,用PyObject实例来描述。Python中的对象有很多种,int类型的对象对应于PyIntObject实例,string类型的对象对应于PyStringObject实例,但是这些PyxxxObject实例同时又都是PyObject实例,假设我们用c++去实现python的话,那么显而易见的是,PyObject是一个基类,各种不同类型的对象实例描述都继承自PyObject。然而,python是由c去实现的,没有类与继承的概念,python的开发者通过精心设计结构体的成员和布局模拟了继承与多态。首先泛化的PyObject结构本身非常简单,共有两个成员ob_refcnt,和ob_type两个成员,其结构如下图:

PyObject
PyObject

其中ob_refcnt 是对象的引用计数,而ob_type指向了对象的类型。

我们再来看一个特化object结构,PyIntObject。它由三个成员组成 ob_refcnt,ob_type,ob_val,其结构如下图:

PyIntObject
PyIntObject

可以看到,PyIntObject的头,就是PyObject结构,这意味着,我们可以通过PyObject* 去引用一个PyIntObject实例。这就实现了在c++中通过基类指针去引用和访问派生类对象的多态特性。更进一步的说,所有的python对象,都包含一个PyObject头,其中存储着这个对象本身的引用计数和所述类型。不同类型的对象可能拥有不同类型的成员,这些成员会存贮在对象内存空间中PyObject头后面的内存里。

Python 对象可以概括的分为两种,定长对象和非定长对象。定长对象指的是这类对象的数据成员长度全部相同,反之,两个同类非定长对象的数据成员是可以不同的。显然,Python int对象就是一个定长python对象,而python string对象就是非定长对象。因为两个python string对象的长度是不相同的。对于非定长的python对象,其内存头部除了保存引用计数和类型之外,还保存了该对象实例本身的数据长度ob_size。

定长对象结构:

变长对象结构:

类型

Python类型在python实现中,用PyTypeObject 实例来描述。Python类型有很多种,每一种类型,都用一个PyTypeObject实例来描述。例如整数类型用PyInt_Type描述,string类型用PyString_Type描述,PyTypeObject中主要存贮了特定Python类型的方法,方法以函数指针的形式存储在PyTypeObject的结构里。

首先应该注意到的是,描述python类型的数据结构中,竟然有一个非定长python对象头,这恰恰印证了,python类型本身也是对象的说法。仔细观察pyTypeObject数据结构,其中大部分的成员是用于保存类型特殊方法的默认的c语言实现的。举例来说tp_new,对应于new 的实现,tp_init 对应于init 的实现。关于类型的方法,在这里暂且按下不表,我们首先关注pyTypeObject中的ob_type 和tp_bases两个成员。Ob_type 成员用于指向一个python对象的python类型。那么作为类对象,其类型就是元类,所以,一个用于描述某种python类型的pyTypeObject实例,其Ob_type 指向的是这个python类型的元类所对应的pyTypeObject实例。默认情况下python类型的元类是type类型,其对应的pyTypeObject实例为 pyType_Type。 tp_bases 指向一个数组,其中存储了python类型的直接基类。如果一个python类型直接继承自object(对应的pyTypeObject 实例为pyBaseObject_Type),那么其对应的pyTypeObject实例的tp_bases 成员中只含有一个指向pyBaseObject_Type 对象的指针。

object与type

现在我们已经知道了,在python类型中有两个特殊的类型,object和type。在python底层分别用pyBaseObject_Type和pyType_Type两个pyTypeObject实例描述。

Object和type的元类都是type,这意味着,pyBaseObject_Type和pyType_Type的obtype都指向pyType_Type。

Object和type的基类都是object,这意味着,pyBaseObject_Type和pyType_Type的tb_bases中都有且只有一个指向pyBaseObject_Type的指针。

python对象系统小结

到此,对于python对象与python类型之间的基本关系可以算是梳理清楚了,在这个基础之上,之后还可以进一步去探索探索自定义类型的创建,类型实例化等内容。但是在这之前,这里我们用一个示意图描述python对象与类型之间的关系。

部分内置python类型对象实现介绍

当我们使用python进行开发时,最长打交道的是各种内置类型的对象。了解常用内置类型的对象实例在python层是如何实现的,有助于我们更加高效的使用python。这一部分,我将整理python最常用的内置类型的对象的底层实现。

int

Python 底层使用PyIntObject 结构来描述python int 对象。PyIntObject是一个定长的python对象,其构造十分简单,只含有一个成员ob_val,保存的就是python int 实例中的数值。

从PyIntObject数据结构可知,Python使用一个long 类型来保存数值。从日常使用python的经验可以知道,python的整数类型是不用担心溢出问题的,然而这里如果单单只用一个long类型去保存数值的话,那么是可能出现溢出情况的。对于这种情况python底层做了透明的类型转换,一旦一个pyIntObject对象包含的数值会发生溢出,那么就重新创建一个pyLongObject 对象去保存数值。

对于常用的python内置对象,另一个不得不提的事情是对象缓存机制。由于python处处皆对象,这意味着为python内置对象申请内存空间的操作将会非常频繁。为了提高性能,针对于大部分常用的python内置类型对象,python底层都有相应的缓存措施。

Python整数对象有两个层次的缓冲措施。第一个措施是针对于小整数。在python2.7中,

范围在[-5,257]的整数对象,一旦创建将永久缓存在系统中。具体的存放位置是一个叫做small_ints的数组。针对于普通的整数对象,python设计了内存块空闲链表缓存所有malloc出的内存并加以管理,这大大减少了由整数对象创建引发malloc的几率。

long

Python 底层使用PyLongObject 结构去描述python的大整数对象。PyLongObject内部用一个数组去保存数值,因此PyLongObject是一个边长对象.

在python源码的注释中,可以看到如何用这个数组去存储一个大整数

“ The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2*(SHIFTi)”

bool

在python中,Bool对象只有两个对象实例Py_True 和Py_False。这是两个PyIntObject对象,数据成员的值分别为1和0。

string

Python用PyStringObject 来描述字符串对象,python字符串对象也是一个变长的数据对象。除了变长对象头,字符串对象还包含了字符数组和字符串常量的哈希值,此外对象内部还有一个state标志位用来标识这个对象有没有被python的intern缓存机制处理。

同整数对象一样,python的字符串对象也有两种缓存机制。分别是针对单个字符的缓冲数组,和针对于所有字符串(包含字符)的intern机制。

Python中所有长度为1的字符串(字符),一旦被创建,就会被永久的缓存到系统中,存储在一个名字为characters的数组里。

而python的字符串intern机制,保证了任何两个字符串变量,如果其字符串内容相同,那么两个名字引用的是用一个字符串对象。

list

Python用PyListObject来描述列表对象,PyListObject的数据结构如下图:

从上图可以看出,PyListObject看起来是一个变长的pyhon对象,从直观理解上,列表对象是变长的这个事情也是说的通的,但是个人在这里倾向于认为PyListObject是定长的列表对象。列表对象维护的数据对象都存储在由ob_item指向的空间,而这段空间与PyListOBject是分离的!所以PyListObject本身应该是定长的。Ob_size在这里用于指出列表当前有多少个对象,allocated用于描述Ob_item所指向的内存空间的总长度。当ob_size等于allocated时意味着内存已满,需要重新分配一块内存给这个列表对象,这个过程与c++ vector容器是一致的。

dict

Python 用PyDictObject来描述字典对象,PyDictObject的数据结构如下图:

可以看到,PyDictObject 一个定长的python对象,注意到PyDictObject中有两个重要的成员,Ma_table 和Ma_smalltable。Ma_table 的作用和list对象中Ob_item的作用是一样的,指向的是真正保存字典元素的空间,当字典中的元素个数小于8时,这片空间就是Ma_smalltable数组!而当字典元素大于8时,系统会另外申请新的内存空间,并将ma_smalltable的内容拷贝过去,最后使ma_table指向新的内存空间。

PyDictObject 保存的元素是一个pyDictEntry, 显然这是一个键值对,其结构如下

可以看到的是,无论是键还是值,保存在PyDictObject中的都仅仅是一个指针。

PyDictObject除了作为python层dict对象的描述,在python底层的实现中,应用也极为广泛。例如string intern缓存,locals 命名空间都是用PyDictOBject实现的。因此其对于搜索的效率要求很高。为此PydictObject采用了散列表的方式存储元素以实现O(1) 复杂度的查找操作,而不是像c++ map容器一样使用红黑树结构。PythonDict容器对象的散列,采用的是开放定址法去实现。当产生散列冲突时,通过二次探测函数,发现下一个候选位置。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 1.元类 1.1.1类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在Python中这...
    TENG书阅读 1,253评论 0 3
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,849评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 早上先生刚醒来,还一脸疲惫。说最近一直加班都没有休息,很累,一直睡不够。 待他洗漱过后,让他坐下,喝点水吃点水果...
    子兰_时空阅读 192评论 0 1
  • 这几天在读《把时间当朋友》,李笑来在第三章管理中讲到了任务拆解。每个人从目标出发,然后寻求达到目标的过程。将过程分...
    花儿朵阅读 1,002评论 1 0