Python Object¶
对象机制¶
- python中对象机制的核心只有两点,p18
- 如何理解这个python的“对象核心机制”?
- 在python中,所有的东西都是对象,而所有的对象都拥有一些相同的内容,p19 图1-1,而这些内容都定义在一个宏之中,p17
- python创建了任何类型的对象后,在其内部,只会用PyOjbect *来指向,就是*ob_type实现了Python这种对象的多态,p25
- 当我们谈论python对象时,要关注两个struct及各自存储的内容(为什么会有如此分离的设计?)
- 对象本身,保存在“对象类型”的定义中,例如PyIntObject定义了整数对象
- 对象元信息,保存在对象对应的“类型对象”中,包括的元信息有p31
其实,无论在“交互模式”还是在“.py模式”使用python时,我们无时不刻不是在访问对象及其元信息:
1 2 3 4 | #访问的是静态的整数对象的定义PyIntObject中的属性ob_ival
>>>a = 1
#访问的是元信息,即“类型对象”中的属性int_doc
>>>print(a.__doc__)
|
- python中,所有的对象都存活在“系统堆”上
类型对象¶
- 每个类型对象其实就是一个PyTypeObject实例
- 类型对象就是面向对象理论中“类”这个概念的实现
对象的行为¶
什么叫“对象的行为”?5+6,a[‘key’], b[0], print(10),等等
为什么数值型、序列型和关联型对象有如此不同的行为?由类型对象中三个函数指针所指向的“函数族”的实现情况决定p23
python允许数值型对象拥有关联型对象的行为吗?允许的,从客户端代码的表现形式就是p23 图1-4;从底层实现来看,p24第一段
对象的行为属于对象的“元信息”,由对应的类型对象来定义
类型对象的初始化¶
为什么python中的“内建类型对象”都是被静态初始化的?意味着它们不在内存的堆栈区,而是在全局区(静态区)p16,所以,类型对象永远不会被析构p27
对象分类¶
p28 图1-7,书中根据什么把python对象分为这5类(类型对象),是对象行为吗?
对象的运行时¶
当谈论对象运行时,可以从两个维度来看:
- 运行时,对象及其类型之间的关系,p25 图1-6
- 对象生命周期(尤其是创建和销毁)各个阶段,在内存中的形态及组织形式
对象和类型之间的关系¶
运行时整数对象及其类型之间的关系,p25 图1-6
对象池¶
- 为什么几乎所有的内建对象,都有自己特有的“对象池机制”?
- 缓解因采用“引用计数机制”,“系统堆”会面临着对象访问瓶颈——当引用计数减为0时,频繁申请、释放内存空间会使python的执行效率大打折扣,p17 ob_refcnt, p43 惊天隐秘
- 对象并非独立对象,而是通过一定结构连接在一起的庞大的对象系统
- 从两个方面来考虑对象池:
1)新建对象的内存空间从哪里来
2)销毁对象时,空间去哪里
INT对象(定长对象)运行时¶
运行时整数对象及其类型之间的关系¶
p25 图1-6
INT对象池¶
全局指针¶
int对象池由三个全局指针来维护,small_ints(p36), block_list和free_list(p37),注意这三个指针的数据类型。
PyIntBlock¶
int对象池的基础结构是PyIntBlock,这个基础结构的构建代码(p36-37,2.2.3)和内存布局(p39图2-4)
在同一个对象池中存在两个单向链表,block_list指向的”block chain”,free_list指向的”空闲PyIntObject对象”链表
基于PyIntBlock,可以回答如下的问题:
- 是否每新建一个int对象,都要在堆中申请一次size(PyIntObject)空间?不是。当free_list=NULL时,表示对象池中无剩余空间,一次性申请size(PyIntBlock)的空间,创建对象池的代码见p38代码清单2-3
- 内存中int对象是否是离散存在的?不是。每个int对象都存在于“PyIntBlock链表”的某个PyIntBlock中。
- 新建的int对象如何利用free_list指向的“空闲内存块单向链表”?如果对象池中存在空闲空间,那么新创建的int对象肯定使用的是free_list指向的那个,这个被使用的空闲空间的ob_type指针转而指向PyInt_Type,以表明这是一个int对象
- 引用为0的int对象的空间是否被释放?不会释放。从两个方面来考虑这个问题:1)本来申请空间时就不是以size(PyIntObject)为单位申请的;2)这种对象会被对象池回收,即重新链入由free_list指向的“空闲内存块单向链表”中,可以参见p41 int_dealloc()实现,以及p41-42对这个函数的解释。
- “空闲空间单向列表”是如何形成的?分为两个阶段,1)初始化,见p38-2.2.4.2-函数fill_free_list();2)当某个int对象的引用计数为0时,见p41 int_dealloc()实现,以及p41-42对这个函数的解释。在整个链接过程中,使用了PyObject中的ob_type指针作为连接指针
小整数和大整数¶
| 小整数 | 大整数 |
| 存储于int block chain中 | 同左 |
| [-5, 257] | 其他 |
| 在程序初始化时就创建好了 | 只在需要时才创建 |
| 内存中只会存在唯一值的小整数对象,p46图2-13 | ob_ival相同的大整数对象使用一次就创建一次, 存于不同的地址,见p45图2-11 |
| 永远不会被释放,因为p43 | ob_refcnt==0就释放 |
变长对象¶
变长对象的实现机制¶
因为对于变长对象而言,对象维护的数据的长度在对象定义时时不知道的。所以,p48,其机制主要由三个不同部分来共同实现:
- 指针,指向变长对象所维护的实际内容的一段内存。在某个具体的“变长对象”中定义,例如,p48,PyStringObject
- ob_size, 这段内存的实际长度(字节)。在PyVarObject中定义
- tp_itemsize,由变长对象保存的元素的单位长度。在类型对象中定义,例如,PyString_Type中tp_itemsize=sizeof(char),p49
String对象¶
内存布局¶
一个string对象是如何创建出来的?PyString_FromString(),这个函数书中分了两个部分来讲,1) p49-代码清单3-1, 2) p52。
- 内存中string header和payload是不连续的空间吗?非也,p51图3-1,由图可得,对象header和对象payload是连续的内存空间。
- 是否每新建一个string对象就申请一次内存空间呢?只要payload的size>1就会申请内存,而无论这个string对象是否在对象池中,p55。然后再进行检查,如果interned dict中存在维护着相同原生字符串的PyStringObject对象,就会释放新申请的堆空间,让对象变量转而指向已经存在于dict中的PyStringObject对象。
- 所有的string对象是松散的存在于内存中吗?非也,python提供一个interned指向的dict,作为部分PyStringObject对象的内存池,称为“intern机制”。
- 这个interned dict是怎么缓存PyStringObject对象的呢,是放在dict的value中吗?非也,dict的key和value都是PyStringObject对象的指针,p54图3-2。
- 哪些string对象可以进入这个dict呢?从p52中创建string对象的后半部分代码可得,只有长度较短的(长度为0或者1)string才能进入和dict缓存。
- 对一个string对象的intern操作包括哪些动作?p53,代码清单3-2
- 一个string对象的引用为0后,会销毁其占用的内存空间吗?会的。无论它是否加入了interned dict中。
List对象¶
PyListObject定义¶
PyListObject是一个变长、可变对象,这两个特征都要靠定义中的成员属性来体现,p63-64
可变对象的实现机制¶
ob_item, allocated&ob_size,这三个成员属性相互协作,实现了list对象的可变以及变长的特性。
allocated和ob_size的值有一些微妙的关系,详细讲解见p64-4.1。
在list的生命周期(对象的创建、对list元素进行插值和删除、list对象的销毁)中,这两个属性(allocated和ob_size)的取值有一些相应的联系,可见p75 4.4前半部分的例子。
创建对象¶
创建对象的代码见p64 代码清单4-1,创建完成后的内存布局见p67 图4-1
- 创建一个list对象包括创建list所维护的元素吗?
非也。创建一个list对象包括了两个部分,且其中,元素指针列表都为NULL
- list维护的“元素指针列表”和“PyListObject对象本身”是连续的内存空间吗?
非也。从p67 图4-1看的一清二楚。如此设计,也是为了实现list的可变对象属性。
- list_a = [1,2,’pyhton’]的实现过程。
应该是经历了两个阶段,1)新建list_a对象(size=4),2)对list_a进行四次“设置元素”的操作。
- 创建list对象时所使用的内存空间来自于对象缓冲池吗?
非也且不全是。首先,从1)list对象内存池的定义p66,以及2)list对象的销毁操作(p74)来看,PyListObject对象缓冲池只是一个“指针数组”而已,先有list对象销毁,才会把其中PyListObject对象本身那一个部分存入缓冲池,也就是说,没有list对象销毁,缓冲池中无内存空间可用;其次,即使从对象缓冲池中获取内存,也只有“PyListObject对象本身”这一部分,而“元素指针列表”部分,还是要重新申请。
对象的销毁和缓冲池¶
- list对象内存池的定义p66
其实,就是一个指针数组
- list对象的销毁操作(p74)
- 销毁list对象的过程以及哪些内存空间可以进入缓冲池?
销毁list可以分为三步:
1) 销毁list维护的元素,各类型的元素有自己的内存回收方法;
2) 销毁list中ob_item指向的“元素指针数组”所占用的内存空间;
3) PyListObject对象本身占用的内存空间归入缓冲池。
- 缓冲池在程序初始化时就申请了内存空间了吗?
非也。list对象的缓冲池本质上就是一个指针数组,在初始化时只是申请了数组相应的全局变量空间。而真正的PyListObject对象空间却是来自于回收的要销毁的list对象的部分内存空间。
insert, append¶
- insert和append操作必然会导致ob_item指向的内存发生变化吗?
不会。是否需要增加ob_item指向的指针数组所占用的空间进行,需要看p69 list_resize()解释。
- 如果需要增加内存空间,是否和insert/append的元素个数相同呢?
非也。从p69 list_resize()可见,ob_item指向的指针数组所增加的内存空间会大于实际insert/append的元素个数。具体例子,可见p75-4.4的前半部分。
Dict对象¶
定义¶
python的dict对象是可变、变长对象。在其struct的定义中肯定要有所体现。
PyDictObject的定义见p79-5.2,内存布局见p81-图5-3。
| PyDictObject | PyListObject |
| 继承自PyObject,并无ob_size这个属性 | 继承自PyVarObject |
| 在对象的连续地址空间中有一个长度为8的PyDictEntry数组 | 元素指针列表和对象自身在不连续的地址空间 |
引入了新的结构体PyDictEntry,用来存储(k,v)的指针。 所以指向可变部分的指针是PyDictEntry *ma_table |
Pyobject **ob_item |
| 为了支持“散列表”实现的关联时容器,加入了多个属性 |
内存池¶
p96-5.4 PyDictObject的内存池实现和PyListObject几乎一模一样。连填充对象缓冲池的方法都是一样的。
关联式容器的数据结构¶
关联式容器刻画着某种对应关系,实现关联式容器的数据结构有很多,例如
- 平衡二元树,
- python采用的“散列表”, p78
python本质上使用了两种数据结构来实现“散列表”
- PyDictEntry, p79-80
- PyDictObject(PyDictEntry *ma_table) , p80
无论采取何种方式来实现“关联式容器”,此容器的核心就是“搜索”,即,通过key找到一个位置,进而得到value
关联式容器的搜索算法¶
理解Dict的搜索¶
关联式容器的数据结构决定了搜索算法,如何理解Dict的搜索,对于理解Dict本质至关重要。
从使用Dict的方式来理解比较直观。我们对Dict无非是插入,设置和删除操作,操作对象都是dict[key],而dict[key]必须返回一个Dict数据结构中的一个位置(绝对无可能是NULL),这个位置本身没有好和不好,而是不同的操作赋予了这个位置不同的意义。例如,
- 同样是返回一个没有被占用的位置,插入就ok,而删除会报错
- 返回一个被占用的位置,d[1]=1就是设置操作
- 返回一个没有被占用的位置,d[1]=1就是插入操作
那么,问题来了,什么决定了dict[key]所返回的位置?
散列表算法的关键概念及python实现¶
散列表算法都是围绕着“从key得到一个位置”,散列表的关键概念如下,p78-5.1
- 散列函数
- 散列值
- 散列冲突
- 冲突解决算法,有“开链法”,“开放定址法”等
- 装载率
“散列函数&冲突解决算法”两者合起来才能决定“一个key对应的位置”——如果散列函数计算的结果没有冲突,那么散列值就是位置;否则,就要采取某种“冲突解决算法”来得到其他的候选位置。
那么,判断“散列冲突”的标准是什么,是否可以脱离“冲突解决算法”来单独讨论什么才叫产生了“散列冲突”?从p83-5.3.2的代码来看,不行。
| 散列函数 | 将key的(修正过的)hash值和entry的数量做一个与操作 |
| 散列值 | 落在entry的数量之下 |
| 散列冲突的判断及解决办法 | 开放定址法 |
| 装载率 |
冲突解决算法的关键概念及python实现¶
开放定址法是python采用的解决散列冲突的方法,p78,算法可以明显的分为三个部分:
- 第一次散列计算
- 判断是否冲突
- 冲突解决
开放定址法中涉及的关键概念:
- 二次探测函数
- 冲突探测链
- 伪删除和entry状态
在开放定址法中,什么才加冲突呢?散列计算(无论是第一次还是冲突解决中的计算)得到的散列值对应的table中的entry
- 是Active状态,有两种可能性,见p84-[4],1)可能就是我们要找的位置;2)继续寻找
- 是Unset状态,就是要找的位置
- 是Dummy状态,继续寻找,这里要用到一个很重要的变量freeslot
开放定址算法的简化示例 p95-5.3.4,一目了然。
| 散列冲突的判断 | entry状态转换,p80 图5-2 |
| 冲突探测链与伪删除 | |
| 二次探测函数 | 根据冲突的位置,对key的hash值进行修正,p86 代码清单5-3 [5] |
| 计算候选位置 | 用修正过的key的hash值带入散列函数得到散列值 |
变长的实现方式¶
Dict靠PyDictEntry *ma_table实现了变长,定义见p80,-5.2.2,图例见p81,图5-3
最佳实践¶
- Dict的key最好用string