这事算是告一段落了
最终总结 最终总结 python中 list 到底是怎么实现的,内存里面是怎么存放的
昨晚和死党聊天的时候,提到一个问题,说python变量的作用域是个有意思的东西,然后就随口说了几句,因为我之前比较熟 c 和 c++ 所以都习惯先定义在使用(Python里面就是先赋值再使用,使用之前我一定要保证他指向的内存里面有我想要的东西),所以一般我都没遇到他说的问题.。然后我就突然来了一句,“你说改变列表值是在原地址上修改呢,还是新开辟内存放一个值,然后把这个内存起个别名”
例如 a = [1,2,3,4]
,a[1] = 20
我想把 2 改成 20 ,是在原来 a[1] 的地址上改呢?还是我从新开辟了一个内存来放 20 这个数,然后给这个内存起了一个别名叫 a[1]
这篇文章的起源就是来自这儿,列表到底是顺序存储还是链式存储,也是个问题
首先看看一个简单的例子
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
a.append(5)
print("添加一个元素后各元素id:")
for i in range(len(a)):
print(id(a[i]))
a.append(6)
print("再添加一个元素后各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
添加一个元素后各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
8791490614432
再添加一个元素后各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
8791490614432
8791490614464
可以看到 a[0]
的 id 是 8791490614304 , a[1]
是8791490614336,一直到 a[4]
是 8791490614432,每两个之间 32 位(4字节,符合我们 int 型 4 字节的印象),看到这个我们猜测 list 是顺序结构的,
问题来了
看下面的例子
a = [1,2,3,4]
print(a)
for i in range(len(a)):
print(id(a[i]))
a[1] = 20
print(a)
print(id(a[1]))
输出
[1, 2, 3, 4]
8791490614304
8791490614336
8791490614368
8791490614400
[1, 20, 3, 4]
8791490614912
我们 a[1] = 20
想把 2 改成 20 ,改完后发现 a[1]
的 id 变了,原来是 8791490614336,现在变成了8791490614912,相差 576bit(位),也就是 72byte(字节),而且输出的列表也的确是变成了20 ,这就怪了
我来点骚操作,我在后面 append 添加16个数(16个数加a[1]
后面的 a[2]
、a[3]
总共18个数刚好72字节)又会是啥情况呢,先试试添加一个是不是从 8791490614400 到 8791490614432
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
a[1] = 20
print("修改后的a[1]id:",id(a[1]))
a.append(5)
print("添加一个元素最后元素id:",id(a[-1]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
修改后的a[1]id: 8791490614912
添加一个元素最后元素id: 8791490614432
看到,的确是 8791490614432 ,好的我们开始骚操作,添加16个后看看情况
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
a[1] = 20
print("修改后的a[1]id:",id(a[1]))
for i in range(16):
a.append(i+5)
print("添加18个元素后各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
修改后的a[1]id: 8791490614912
添加18个元素后各元素id:
8791490614304
8791490614912
8791490614368
8791490614400
8791490614432
8791490614464
8791490614496
8791490614528
8791490614560
8791490614592
8791490614624
8791490614656
8791490614688
8791490614720
8791490614752
8791490614784
8791490614816
8791490614848
8791490614880
8791490614912
8791490614912 出现了两次,而且和我们预料的最后一个应该是 8791490614912 也完全一致
此时输出 a 的所有元素是这样的
[1, 20, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
那么为什么;8791490614912 出现了两次呢,原因我想我大概清楚了,因为 地址(id)是 8791490614912 的这块内存有两个别名 a[1]
、a[19]
,内容都是20,
所以为什么修改成 20 地址会变到 8791490614912呢,我觉得应该是在原来的地址上往后移动了 20(目标值)-2(初始值)
个整数所占的字节,这样一来似乎 list 就是顺序存储的
但是结论还为实过早,多做下实验
为了观察下先删除最后一个再添加一个是啥情况,做了下面实验,分别测试下pop()、 remove()、del()
pop()
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
poplist = a.pop() #把 4 删掉
a.append(4) # 在[1,2,3]后面加一个4
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
print("pop方法返回值的id:",id(poplist))
输出
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
pop方法返回值的id: 8791490614400
可以看到原来列表的最后一个元素的 id 8791490614400里面还是有数的,而且这个数是4(原来列表的最后一个元素值),poplist = a.pop()
只是把这个地址叫另一个别名 poplist ,然后我 append 一个 4 到列表后面,id 还是原来的 8791490614400。说明添加是在原来的地址 a[-1](即a[3])
往后移动了4(添加的值)- 3(原列表最后一个元素的值)
个整数所占的字节,再次说明是顺序存储的
为了证明我说的 添加是在原来的最后地址 a[-1]
往后移动了添加的值 - 原列表最后一个元素的值
个整数所占的字节, 我再 append 一次看看情况
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
poplist = a.pop()
a.append(4)
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
print("pop方法返回值的id:",id(poplist))
a.append(10)
print("再添加一次的各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
pop方法返回值的id: 8791490614400
再添加一次的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
8791490614592
可以看到从 8791490617400 跳到了8791490614592,192 / 32 = 6,6 = 10 - 4,10是添加的值,4是原列表最后一个值,是不是证明了我的观点,你可以试试 添加 104, id 肯定相差 32 *(104 - 4)= 3200位
再看看 remove() 是不是也一样呢
remove()
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
a.remove(4) #把 4 删掉
a.append(4) # 在[1,2,3]后面加一个4
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
可以看到,前后 id 是一样的,和 pop 也一样
再来一个移除中间的,比如2
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
a.remove(2)
a.append(2)
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614368
8791490614400
8791490614336
最后一个 id 是原来 a[1] 的 id ,说明我总结的 添加是在原来的最后地址 a[-1]
往后移动了添加的值 - 原列表最后一个元素的值
个整数所占的字节,是对的,差是正数就向后移,差是负数,向后移负数个,不就是向前移吗
最后看看del会有啥不同不
del()
删除最后的
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
del(a[3])
a.append(4)
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除中间的
a = [1,2,3,4]
print("原始的各元素id:")
for i in range(len(a)):
print(id(a[i]))
del(a[1])
a.append(2)
print("删除后再添加的各元素id:")
for i in range(len(a)):
print(id(a[i]))
输出
原始的各元素id:
8791490614304
8791490614336
8791490614368
8791490614400
删除后再添加的各元素id:
8791490614304
8791490614368
8791490614400
8791490614336
也一样,三种删除都试过了,都是一样的
总结
- list (列表)是顺序存储的
- 修改列表元素的值:在原来的地址上往后移动了
目标值 - 原始值
个整数(列表数据类型)所占的字节,用来存放目标值,然后给这块地址起别名,别名就是原始值的索引。比如a[1] = 20
,别名就叫a[1]
- 添加:在原来的最后地址
a[-1]
往后移动了添加的值 - 原列表最后一个元素的值
个整数(列表数据类型)所占的字节,用来存放要添加的值,然后起个别名。这儿的别名是原列表的最后索引+1,比如原来列表有四个元素,最后索引是a[3]
,进行一次添加操作后,新添加的这个元素的别名就是a[4]
- 差是正数就向后移,差是负数,向后移负数个,不就是向前移吗
这儿有一些简单讨论 https://www.v2ex.com/t/484389
这儿是 github上 python实现 list 的源码 https://github.com/python/cpython/blob/3.7/Include/listobject.h#L23