Python小知识(二)

  1. 可变字符串
    在 Python 中,字符串属于不可变对象,不支持修改,如果需要修改其中的值,只能创建新的字符串对象。但是,经常我们确实需要原地修改字符串,可以使用 io.StringIO 对象或 array 模块。
  • StringIO 顾名思义就是在内存中读写 String

关于StringIO 和 BytesIO

  1. List 中的 append() 和 extend()
  • append() :在原列表尾部添加新的元素,速度最快,推荐使用
  • + :并不是真正的尾部添加元素,而是创建新的列表对象,将两个列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用
  • extend() :将目标列表的所有元素添加到本列表的尾部,直接操作原对象,不创建新的列表对象

  1. 字典底层存取值原理

在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 即可,里面每一行都是有用的数据,不存在跳过的情况,减少了遍历的个数,内存空间也得到很好的利用。

发布了21 篇原创文章 · 获赞 6 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/codedancing/article/details/103864803
今日推荐