近期看到V2EX上一个老帖子 新人求教: Python 删除 dict 一个 item 后,内存不释放的?
引起了我的思考,如果对一个dict对象进行增删改操作,它的内存占用会发生什么样的变化?

运行环境 Runtime environment

1
2
3
操作系统: Windos10  
IDE: JetBrains Pycharm 2019.2.4 x64
语言: Python 3.7.4

背景

最近因为开发的需要,需要对内存进行优化。
尤其是在使用异步操作全局变量增删改查的时候可能带来的内存冗余问题。
近期看到V2EX上一个老帖子 新人求教: Python 删除 dict 一个 item 后,内存不释放的?
引起了我的思考,如果对一个dict对象进行增删改查操作,它的内存占用会发生什么样的变化?
而Python中的字典又是内存占用大户,怎么用才能省内存呢?

创建字典

探究过程中需要使用到sys,copy,time,random这几个包

1
2
3
4
5
6
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小

运行结果:

1
2
3
4
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:1544631697064
字典在内存所占大小:240 字节

试着多运行了几次,得到的结论:字典创建出来,哪怕它是空字典,它就已经占用了240字节内存了。

字典增加键值对

如果给这个字典对象添加新的键值对(key,value)会不会让字典变大呢?
增加后的字典内容我就不打印了,太长了卡死我了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
for i in range(10):
key = int(random.random() * 1000)
n[key] = [time.time()] * 100000
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:1588570767016
字典在内存所占大小:240 字节
**************************************************
变量类型:<class 'dict'>
字典的内存id:1588570767016
字典在内存所占大小:368 字节

结论:

  • dict的id和类型不会有变化,被修改的就是字典本身而不是新生成了一个有键值对的字典
  • 添加键值对较少时,dict的大小还是240字节(测试的代码就不贴了)
  • 添加大量键值对时,dict的大小是会上升的

字典修改键值对内容

键值对弄多一点,把字典中键值对的值全部改为None,对比一下修改前后内存占用有没有变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
for i in range(50):
key = int(random.random() * 1000)
n[key] = [time.time()] * 100000
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
for k,v in n.items():
n[k]=None
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:2327090183848
字典在内存所占大小:240 字节
**************************************************
变量类型:<class 'dict'>
字典的内存id:2327090183848
字典在内存所占大小:2280 字节
**************************************************
变量类型:<class 'dict'>
字典的内存id:2327090183848
字典在内存所占大小:2280 字节

结论:

  • dict的id和类型不会有变化
  • dict的内存占用如果被搞大了,就算把所有的键的值改成None,它也小不回去了..

字典删除键值对

把字典里面的一部分键值对删除了,对比一下内存大小的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
for i in range(50):
key = int(random.random() * 1000)
n[key] = [time.time()] * 100000
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
keylist = list(n.keys())[1:30] # 删除其中30个键值对
for k in keylist:
del n[k]
# n[k].clear()
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:2133858860712
字典在内存所占大小:240 字节
**************************************************
变量类型:<class 'dict'>
字典的内存id:2133858860712
字典在内存所占大小:2280 字节
**************************************************
变量类型:<class 'dict'>
字典的内存id:2133858860712
字典在内存所占大小:2280 字节

结论:

  • dict的id和类型不会有变化
  • dict用del方法删除了一部分键值对,dict也不会变小
  • dict用.clear()方法删除了一部分键值对,dict也不会变小(结果就不贴出了)

字典深拷贝

python中的dict是可变类型,想要新生成一个新的、内容一样的字典需要用到深拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('*'*50)
m = copy.deepcopy(n)
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(m)) # 变量类型
print('字典的内存id:%s'%id(m)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(m)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
9
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:2131133152936
字典在内存所占大小:240 字节
**************************************************
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:2131163151320
字典在内存所占大小:240 字节

结论:

  • 字典id有变化
  • 深拷贝的字典是全新的字典
  • 新字典与前字典的内容和内存占用大小是完全相同的

# 字典变化以后再深拷贝

如果说前一个字典存了很多键值对,我给它删了一部分,
我再把删过的前字典再深拷贝成一个新字典,
新字典它的内存占用与前字典比较会变化(小)吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('添加一些键值对'+'*'*50)
for i in range(50):
key = int(random.random() * 1000)
n[key] = [time.time()] * 100000
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('删除部分键值对'+'*'*50)
keylist = list(n.keys())[1:30] # 删除其中30个键值对
for k in keylist:
n[k].clear()
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('深拷贝'+'*'*50)
m = copy.deepcopy(n)
# print('字典内容:%s'%m) # 字典内容
print('变量类型:%s'%type(m)) # 变量类型
print('字典的内存id:%s'%id(m)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(m)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:1921112555176
字典在内存所占大小:240 字节
添加一些键值对**************************************************
变量类型:<class 'dict'>
字典的内存id:1921112555176
字典在内存所占大小:2280 字节
删除部分键值对**************************************************
变量类型:<class 'dict'>
字典的内存id:1921112555176
字典在内存所占大小:2280 字节
深拷贝**************************************************
变量类型:<class 'dict'>
字典的内存id:1921113586648
字典在内存所占大小:2280 字节

结论:

  • 深拷贝后的字典id有变化
  • 深拷贝的字典是全新的字典
  • 深拷贝真的是把前字典一切都搬过来了,哪怕前字典删除了键值对,内存大小还是照搬了最大时候的占用

建立新字典遍历插入旧字典键值对

由上面可知深拷贝除了id有变化创建成新字典外,其他全部都继承了旧字典的衣钵,内存大小不会有变化。
那么新创一个字典,不用深拷贝,然后通过遍历的方式把旧字典删除部分键值对后的内容复制一遍,新字典还跟旧字典大小一样吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import sys,copy,time,random
n = dict() # 创建空字典对象
print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('添加一些键值对'+'*'*50)
for i in range(50):
key = int(random.random() * 1000)
n[key] = [time.time()] * 100000
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('删除部分键值对'+'*'*50)
keylist = list(n.keys())[:-1] # 删除其中30个键值对
for k in keylist:
del n[k]
# print('字典内容:%s'%n) # 字典内容
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小
print('新字典遍历插入'+'*'*50)
m = dict()
for k,v in n.items():
m[k]=v
# print('字典内容:%s'%m) # 字典内容
print('变量类型:%s'%type(m)) # 变量类型
print('字典的内存id:%s'%id(m)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(m)) # 字典在内存所占大小
print('销毁旧字典并将新创建字赋值到旧变量'+'*'*50)
del n
n = m
print('变量类型:%s'%type(n)) # 变量类型
print('字典的内存id:%s'%id(n)) # 字典的内存id
print('字典在内存所占大小:%s 字节'%sys.getsizeof(n)) # 字典在内存所占大小

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
字典内容:{}
变量类型:<class 'dict'>
字典的内存id:2478120058536
字典在内存所占大小:240 字节
添加一些键值对**************************************************
变量类型:<class 'dict'>
字典的内存id:2478120058536
字典在内存所占大小:2280 字节
删除部分键值对**************************************************
变量类型:<class 'dict'>
字典的内存id:2478120058536
字典在内存所占大小:2280 字节
新字典遍历插入**************************************************
变量类型:<class 'dict'>
字典的内存id:2478120306312
字典在内存所占大小:240 字节
销毁旧字典并将新创建字赋值到旧变量**************************************************
变量类型:<class 'dict'>
字典的内存id:2478120306312
字典在内存所占大小:240 字节

结论:

  • 要想让dict内存占用变小,只能创建新的dict把旧dict内容遍历添加进去。

总结

很少写这么长博客..太耗时间了,总结一下观察结果吧。

  • python的dict(字典)类型,是没有弹性的。它对内存大小的占用只会越来越大,不会因为键值对被删除而缩小。
  • 删除dict键值对以后若是需要压缩内存占用大小,只能创建新的dict把旧dict内容遍历添加进去。
  • dict深拷贝是将旧字典的最大内存占用和dict里的内容全部都照搬到一个新字典上,对内存缩小没有作用。