Python进阶——数据类型的底层实现

第一部分 从奇怪的列表说起

1.1 错综复杂的复制

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
# list_3 = list_1          # 错误!!!只是取了别名
list_2 = list_1.copy()     # 或者list_1[:] 或者list(list_1) 均可实现浅拷贝

# 对浅拷贝前后的列表中的列表进行操作,发现两个列表都变了
list_2[1].append(55)
print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 44, 55], (5, 6, 7), {'name': 'Sarah'}]
list_2:   [1, [22, 33, 44, 55], (5, 6, 7), {'name': 'Sarah'}]

1.2 列表的底层实现

1.2.1 引用数组

  • 列表存储的,实际上是元素的地址!!!
    元素可以分散的存储在内存中,地址的存储在内存中是连续的。
  • 如果直接把变量名赋值给另个一个变量,相当于没有变化,只不过又起了个名字。
  • 当通过浅拷贝把list_1赋值给list_2时,相当于list_2拿到了一份与list_1独立的位置信息,但是指向的内容是完全一样的。
  • list_1[0]中存储的是一个数字的地址。
    list_1[1]中存储的是一个地址列表的地址,地址列表中存储的是三个数字的地址。
    list_1[2]中存储的是一个地址元组的地址,地址元组中存储的是三个数字的地址。
    list_1[3]中存储的是一个字典的散列表的地址,字典的值就存在散列表中。

1.2.2 新增元素

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = list(list_1)   # 浅拷贝   与list_1.copy()功能一样

# 两个列表分别新增元素
list_1.append(100)
list_2.append("n")print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah'}, 100]
list_2:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah'}, 'n']
  • 新增元素时,相当于在两个列表中分别新存了一个地址,所以可以达到预期的效果。

1.2.3 修改元素

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = list(list_1)

list_1[0] = 10
list_2[0] = 20print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [10, [22, 33, 44], (5, 6, 7), {'name': 'Sarah'}]
list_2:   [20, [22, 33, 44], (5, 6, 7), {'name': 'Sarah'}]
  • 当给list_1[0]重新赋值时,相当于把1的地址替换成了10的地址,list_2同理。

1.2.4 对列表型元素进行操作

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = list(list_1)

list_1[1].remove(44)
list_2[1] += [55, 66]    # list_2[1] += [55, 66]等价于append(),但是不等价于list_2[1] = list_2[1]+[55, 66]
                         # 前者在原列表上进行操作,后者返回新的列表,是可以实现预期效果的print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 55, 66], (5, 6, 7), {'name': 'Sarah'}]
list_2:   [1, [22, 33, 55, 66], (5, 6, 7), {'name': 'Sarah'}]
  • list_1[1]和list_2[1]都指向同一个地址列表,地址列表指向3个元素,对地址列表的操作,是改变了地址列表中的元素的指向,但是list_1[1]和list_2[1]的指向没有发生变化。

1.2.5 对元组型元素进行操作

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = list(list_1)

list_2[2] += (8,9)print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah'}]
list_2:   [1, [22, 33, 44], (5, 6, 7, 8, 9), {'name': 'Sarah'}]
  • 元组是不可变的,一旦定义好,就不能进行修改了。list_2[2]在执行加法操作后,产生了一个新的元组,元组中存储的是5个数字的地址,list_2[2]指向新的元组,list_1[2]仍然指向原来的元组。

1.2.6 对字典型元素进行操作

list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = list(list_1)

list_1[-1]["age"] = 18print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah', 'age': 18}]
list_2:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah', 'age': 18}
  • list_1[-1]中存储的是一个字典的散列表的地址,散列表中存储了一个值 ‘Sarah’。增加一个值,是在散列表中通过键找到相应的位置,把值存进去,散列表的地址没有变。

1.2.7 小结

  • 有些数据类型,如列表和字典,是可变的,它可以地址不变而内容变化。
  • 不可变的数据类型,如果内容变化,地址也会变化。

1.3 深拷贝——copy.deepcopy(列表)

  • 浅拷贝之后
    针对不可变元素(数字、字符串、元组)的操作,都各自生效了。
    针对不可变元素(列表、字典、集合)的操作,发生了一些混淆。
  • 引入深拷贝
    深拷贝将所有层级的相关元素全部复制,完全分开,泾渭分明,避免了上述问题。
import copy
​
list_1 = [1, [22, 33, 44], (5, 6, 7), {"name": "Sarah"}]
list_2 = copy.deepcopy(list_1)
list_1[-1]["age"] = 18
list_2[1].append(55)print("list_1:  ", list_1)
print("list_2:  ", list_2)
list_1:   [1, [22, 33, 44], (5, 6, 7), {'name': 'Sarah', 'age': 18}]
list_2:   [1, [22, 33, 44, 55], (5, 6, 7), {'name': 'Sarah'}]

第二部分 神秘的字典

2.1 快速的查找

import time
​
ls_1 = list(range(1000000))
ls_2 = list(range(500))+[-10]*500
​
start = time.time()
count = 0
for n in ls_2:
    if n in ls_1:
        count += 1
end = time.time()
print("查找{}个元素,在ls_1列表中的有{}个,共用时{}秒".format(len(ls_2), count,round((end-start),2)))
查找1000个元素,在ls_1列表中的有500个,共用时3.92秒
import time
​
d = {i:i for i in range(100000)}
ls_2 = list(range(500))+[-10]*500
​
start = time.time()
count = 0
for n in ls_2:
    try:
        d[n]
    except:
        pass
    else:
        count += 1
end = time.time()
print("查找{}个元素,在ls_1列表中的有{}个,共用时{}秒".format(len(ls_2), count,round(end-start)))
查找1000个元素,在ls_1列表中的有500个,共用时0秒
  • 分别用列表和字典实现1000个元素的查找,字典比列表快得多,why?

2.2 字典的底层实现

  • 字典通过稀疏数组来实现值的存储与访问

2.2.1 字典的创建过程

  • 第一步:创建一个散列表(稀疏数组 长度 >> 元素个数)
d = {}
  • 通过hash()计算键的散列值
print(hash("python"))
print(hash(1024))
print(hash((1,2)))
1989505856890947430
1024
3713081631934410656
  • 增加键值对的操作,首先会计算键的散列值
d["age"] = 18
print(hash("age")) 
-4940077774348628352
  • 第二步:根据计算的散列值确定其在散列表中的位置
    极个别时候,散列值会发生冲突,则内部有相应的解决冲突的办法
  • 第三步:在该位置上存入值

2.2.2 键值对的访问过程

d["age"]
  • 第一步:计算要访问的键的散列值
  • 第二步:根据计算的散列值,通过一定的规则,确定其在散列表中的位置
  • 第三步:读取该位置上存储的值
    如果存在,则返回该值
    如果不存在,则报错KeyError

2.3 小结

  • 字典数据类型,通过空间换时间,实现了快速的数据查找
    也就注定了字典的空间利用效率低下
  • 因为散列值对应位置的顺序与键在字典中显示的顺序可能不同,因此表现出来字典是无序的
  • 为什么要用稀疏数组?
    如果长度刚好,会产生很多位置冲突,增加了处理效率
  • 列表是从头开始查找的,字典只需要计算散列值就可以确定位置,所以字典实现了比列表更快速的查找

第三部分 紧凑的字符串

  • 通过紧凑数组实现字符串的存储
    数据在内存中是连续存放的,效率更高,节省空间
  • 同为序列类型,为什么列表采用引用数组,而字符串采用紧凑数组?
    字符串中每个元素是单一的字符,大小可控,方便预留空间;列表元素多种多样,没有办法预留空间,不如把数据存储在其它位置,列表中只存储地址。

第四部分 是否可变

4.1 不可变类型:数字、字符串、元组

  • 在生命周期中保持内容不变
    换句话说,改变了就不是它自己了(id变了)
    不可变对象的 += 操作 实际上创建了一个新的对象
x = 1
y = "Python"
print("x id:", id(x))
print("y id:", id(y))

x += 2
y += "3.7"
print("x id:", id(x))
print("y id:", id(y))
x id: 140718440616768
y id: 2040939892664
x id: 140718440616832
y id: 2040992707056
  • 元组并不是总是不可变的
    元组中的元素是不可变类型的时候,元组才是不可变的,如果元组中有可变元素,如列表,那么元组还是可变的。
t = (1,[2])
t[1].append(3)
print(t)
(1, [2, 3])

4.2 可变类型:列表、字典、集合

  • id 保持不变,但是里面的内容可以变
  • 可变对象的 += 操作,实际在原对象的基础上就地修改
ls = [1, 2, 3]
d = {"Name": "Sarah", "Age": 18}
print("ls id:", id(ls))
print("d id:", id(d))

ls += [4, 5]
d_2 = {"Sex": "female"}
d.update(d_2)            # 把d_2 中的元素更新到d中
print("ls id:", id(ls))
print("d id:", id(d))
ls id: 2040991750856
d id: 2040992761608
ls id: 2040991750856
d id: 2040992761608

第五部分 列表操作的几个小坑

5.1 删除列表内的特定元素

  • 存在运算删除法
    缺点:每次存在运算,都要从头对列表进行遍历、查找、效率低
alist = ["d", "d", "d", "2", "2", "d" ,"d", "4"]
s = "d"
while True:
    if s in alist:
        alist.remove(s)
    else:
        break
print(alist)
['2', '2', '4']
  • 一次性遍历元素执行删除(错误)
alist = ["d", "d", "d", "2", "2", "d" ,"d", "4"]
for s in alist:
    if s == "d":
        alist.remove(s)      # remove(s) 删除列表中第一次出现的该元素
print(alist)
['2', '2', 'd', 'd', '4']
  • for循环会记忆索引,删除第一个 “d” 后,列表变成 [“d”, “d”, “2”, “2”, “d” ,“d”, “4”],for循环记忆的是,取过第0个元素了,接下来要取第一个元素,也就是新列表的第0个元素被略过了,以此类推。

负向索引

alist = ["d", "d", "d", "2", "2", "d" ,"d", "4"]
for i in range(-len(alist), 0):
    if alist[i] == "d":
        alist.remove(alist[i])      # remove 删除列表中第一次出现的该元素
print(alist)
['2', '2', '4']

5.2 多维列表的创建

ls = [[0]*10]*5
print(ls)

ls[0][0] = 1
print(ls)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
  • [0]*10 本身是一个列表,对其进行复制,指向的是同一个列表。

解析语法

ls = [[0]*10 for i in range(5)]
print(ls)

ls[0][0] = 1
print(ls)
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值