python修改列表的值时的地址变化,一个蛮有意思的小实验

这事算是告一段落了
        

2020.9.6号更新

         最终总结 最终总结 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

posted on 2021-06-09 19:59  雾恋过往  阅读(218)  评论(0编辑  收藏  举报

Live2D