- 可变字符串
在 Python 中,字符串属于不可变对象,不支持修改,如果需要修改其中的值,只能创建新的字符串对象。但是,经常我们确实需要原地修改字符串,可以使用io.StringIO
对象或array
模块。
-
StringIO
顾名思义就是在内存中读写String
- List 中的 append() 和 extend()
-
append()
:在原列表尾部添加新的元素,速度最快,推荐使用 -
+
:并不是真正的尾部添加元素,而是创建新的列表对象,将两个列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用 -
extend()
:将目标列表的所有元素添加到本列表的尾部,直接操作原对象,不创建新的列表对象
- 字典底层存取值原理
在Python 3.6以前,字典是不能保证顺序的,但是从Python 3.6开始,字典是有顺序的。从Python 3.6开始,字典占用内存空间的大小,视字典里面键值对的个数,只有原来的30%~95%
在Python 3.6之前,字典的底层原理:
字典对象的核心是散列表
,散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket,每个 bucket 有两部分:一个是键对象的引用
,一个是值对象的引用
。所有 bucket 结构和大小一致,我们可以通过偏移量来读取指定 bucket。
要把 ”name1” = ”coded”
和 "name2" = "dancing"
这两个键值对放到字典对象 a 中,首先第一步需要计算键 ”name1”
的散列值。
Python 中可以通过 hash()
来计算:
>>> a = {}
>>> a["name1"] = "code"
>>> a["name2"] = "dancing"
>>> hash("name1")
-6135015113801533726
>>> hash("name2")
-7977426291076884919
>>> bin(hash("name1"))
'-0b101010100100011111100111011110111000101110010110000100100011110'
>>> bin(hash("name2"))
'-0b110111010110101100000101111001000001101011111111110100110110111'
字典 a 对象创建完后,CPython 的底层会初始化一个二维数组,这个数组有 8 行 3 列。存放 “name1” = “code” 的过程:
- 第一个值为
hash("name1”)
在当前运行时
的hash值( -6135015113801533726 )
Python自带 hash 函数计算出来的值,只能保证在每一个运行时内不变,但是当关闭Python解释器再重新打开,那么它的值就可能会改变
- 第二个值为
"name1"
这个字符串所在的内存的地址(指针就是内存地址) - 第三个值为
"code"
这个字符串所在的内存的地址
用 bin(hash("name1"))
计算出的散列值的最右边 3 位数字作为偏移量,即 “110”,十进制是数字 6 ,然后查看偏移量 6,对应的 bucket 是否为空,如果为空,则将键值对放进去,如果不为空,则依次取向左 3 位作为偏移量,即“011”
,十进制是数字 3, 依次类推。
Python 会根据散列表的拥挤程度扩容。创造更大的数组,将原有内容拷贝到新数组中,接近
2/3
时,数组就会扩容,8行变成16行,16行变成32行。长度变了以后,原来的余数位置也会发生变化,此时就需要移动原来位置的数据,导致插入效率变低。
为了解决 Hash 碰撞,Python 为了不覆盖之前已有的值,就会使用开放寻址法
重新寻找一个新的位置存放这个新的键值对。
假设我们要读取 name1 对应的值。
此时,Python先计算在当前运行时下面,name1 对应的 Hash 值是多少:
>>> hash("name1")
-6135015113801533726
>>> bin(hash("name1"))
'-0b101010100100011111100111011110111000101110010110000100100011110'
“110” 十进制为 6 ,那么二维数组里面,找到下标为 6 的这一行,对键通过__eq__()
方法检测相等性,如果相等直接返回这一行第三个指针对应的内存中的值,就是 name1 对应的值 code,如果不相等,则向左三位 “011” 得十进制 3,找到下标为 3 的这一行,依次类推。
当循环遍历字典的 Key 的时候,Python 底层会完整遍历这个二维数组,如果当前行有数据,那么就返回 Key 指针对应的内存里面的值,如果当前行没有数据,那么就跳过。二维数组每一行有三列,每一列占用 8 byte 的内存空间,所以每一行会占用 24 byte 的内存空间。
在Python 3.6及以后,字典的底层原理:
当初始化一个空的字典以后,Python 单独生成了一个长度为 8 的一维数组 和 一个空的二维数组:
indices = [None, None, None, None, None, None, None, None]
entries = []
当”name1” = ”coded”
键值对放到字典对象 a 中时:
>>> hash("name1")
-6135015113801533726
>>> bin(hash("name1"))
'-0b101010100100011111100111011110111000101110010110000100100011110'
“110” 十进制为 6,把 indices 这个一维数组里面,下标为 6 的位置修改为 0 。这里的 0 是二位数组 entries 的索引。现在 entries 里面只有一行三列:name1 的hash值、指向name1的指针 和 指向code的指针。所以 indices 里面填写的数字 0,就是刚刚我们插入的这个键值对的数据在二维数组里面的行索引。
indices = [None, None, None, None, None, None, 0, 1]
entries = [
[-6135015113801533726, 指向name1的指针, 指向code的指针],
[-7977426291076884919, 指向name2的指针, 指向dancing的指针]
]
假如要读取 name1 的值,那么首先计算 name1 的hash值,以及这个值对 8 的余数得出余数 6, 那么去读 indices 下标为6的这个值。这个值为 0,然后再去读 entries 里面,下标为 0 的这一行的数据,也就是 name1 对应的数据了。
新的这种方式,当插入新的数据的时候,始终只是往 entries 的后面添加数据,这样就能保证插入的顺序。当遍历字典的 Keys 和 Values 的时候,直接遍历 entries 即可,里面每一行都是有用的数据,不存在跳过的情况,减少了遍历的个数,内存空间也得到很好的利用。