Python 字典内存释放及其浅拷贝和深拷贝之间的区别

更多、更及时内容欢迎留意微信公众号小窗幽记机器学习

背景

在用Python搭建服务过程使用字典存放自定义的对象,需要特别指出的是value值是占用内存空间较大的对象。随着时间的流逝和数据的累积,字典的key变得越来越多,从而使得整个字典对象占用过大的内存空间。此时,需要根据实际需要定期删除特定的keys,及时释放内存,否则就可能引发血案:OOM,进程被kill。

字典内存释放

众所周知,去掉字典中元素可以使用 pop 或者 del 方法,但是这两种方法都没有真正地释放对应的元素的空间。Python 为了保证hash table 探测链的完整,对于那个被删除的key只是被标记成了空,并没有真正被删除掉,所以该字典的内存占用没有得到释放。这是为了避免多度重建hash table。那么,如何真正释放这部分内存空间呢?可以创建或者拷贝一个旧字典再覆盖掉新字典。具体示例如下:

import sys
import gc
import copy
a = {}
print("init empty dict memory size={} bytes".format(sys.getsizeof(a)))

for i in range(10**6):
    a[i] = i
print("after set value, dict memory size={} bytes".format(sys.getsizeof(a)))

for i in range(10**6):
    del a[i]
    # a.pop(i)

print("after del, dict memory size={} bytes".format(sys.getsizeof(a)))
a_new = dict(a)
print("after init a new one, dict memory size={} bytes".format(sys.getsizeof(a_new)))
b = copy.copy(a)
print("after copy a new one, dict memory size={} bytes".format(sys.getsizeof(b)))
c = copy.deepcopy(a)
print("after deepcopy a new one, dict memory size={} bytes".format(sys.getsizeof(c)))

运行结果如下:

init empty dict memory size=240 bytes
after set value, dict memory size=41943144 bytes
after del, dict memory size=41943144 bytes
after init a new one, dict memory size=240 bytes
after copy a new one, dict memory size=240 bytes
after deepcopy a new one, dict memory size=240 bytes

笔者在实验过程中还发现一个有趣的问题:字典调用 clear 操作后的内存占用比新建一个字典的内存占用小。具体示例如下:

dict = {}
print(sys.getsizeof(dict))  # 240, 这因为新的字典的 size 是 PyDict_MINSIZE
dict.clear()
print(sys.getsizeof(dict))  # 72

这是因为新建字典是按照PyDict_MINSIZE 分配keyspace。当调用.clear()函数后,keyspace 被重新分配到一个静态的空keyspace: Py_EMPTY_KEYS,此时的字典是真的empty。

上述示例代码中使用浅拷贝和深拷贝的方式释放空间,那么这里进一步介绍下直接赋值、浅拷贝和深拷贝之间的区别。

浅拷贝 Vs 深拷贝

简而言之:

  • 直接赋值:其实就是对象的引用(别名)。

  • 浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。

  • 深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。

浅拷贝

浅拷贝的示例代码如下:

class test_obj:
    def __init__(self, obj_id, text, label='0'):
        self.obj_id = obj_id
        self.text = text
        self.label = label


ob1 = test_obj("id1", "文本1", 'label1')
a = {"k1": "12345", "k2": [0, 1, 2], "k3": ob1}
b = a.copy()  # 浅拷贝
print("b=", b)
a["k2"].append(4)  # 由于是浅拷贝,所以key所引用的对象是相同的,所以会造成b的值也随a变化
print("Append a")
print("a=", a)
print("b=", b)

# 如果是 pop 掉key的话
print("Pop b:")
b.pop("k2")
# a.pop("k2")
print("a=", a)
print("b=", b)

# 修改值
print("Modify object value:")
print("values of a=", vars(a["k3"]))
b["k3"].text = "换文本了"
print("values of a=", vars(a["k3"]))

运行结果如下:

b= {'k1': '12345', 'k2': [0, 1, 2], 'k3': <__main__.test_obj object at 0x7f1aa09e8f60>}
Append a
a= {'k1': '12345', 'k2': [0, 1, 2, 4], 'k3': <__main__.test_obj object at 0x7f1aa09e8f60>}
b= {'k1': '12345', 'k2': [0, 1, 2, 4], 'k3': <__main__.test_obj object at 0x7f1aa09e8f60>}
Pop b:
a= {'k1': '12345', 'k2': [0, 1, 2, 4], 'k3': <__main__.test_obj object at 0x7f1aa09e8f60>}
b= {'k1': '12345', 'k3': <__main__.test_obj object at 0x7f1aa09e8f60>}
Modify object value:
values of a= {'obj_id': 'id1', 'text': '文本1', 'label': 'label1'}
values of a= {'obj_id': 'id1', 'text': '换文本了', 'label': 'label1'}

浅拷贝下,a 和 b 是一个独立的对象,但他们的子对象还是指向统一对象(即为引用)。所以,子对象的值改变的话,两者都会改变;而 pop 或者 del 特定key,则由于是独立对象不会相互影响。所以当需要在程序中动态地删除某些key(特别是key所对应的value占用较大内存空间),且要释放内存空间的话可以使用浅拷贝的方式重新赋值,否则单纯 pop 特定key并不会释放字典的内存空间。

深拷贝

深拷贝, a 和 b 完全拷贝了父对象及其子对象,两者是完全独立的。
深拷贝的示例代码:

import copy

class test_obj:
    def __init__(self, obj_id, text, label='0'):
        self.obj_id = obj_id
        self.text = text
        self.label = label


ob1 = test_obj("id1", "文本1", 'label1')
a = {"k1": "12345", "k2": [0, 1, 2], "k3": ob1}
b = copy.deepcopy(a)
print("a=", a)
print("b=", b)
a["k2"].append(4)
print("Append a")
print("a=", a)
print("b=", b)

# 如果是 pop 掉key的话
print("Pop b:")
b.pop("k2")
# a.pop("k2")
print("a=", a)
print("b=", b)

# 修改值
print("Modify object value:")
b["k3"].text = "换文本了"
print("values of a=", vars(a["k3"]))
print("values of b=", vars(b["k3"]))

运行结果如下:

a= {'k1': '12345', 'k2': [0, 1, 2], 'k3': <__main__.test_obj object at 0x7fbf660cdf98>}
b= {'k1': '12345', 'k2': [0, 1, 2], 'k3': <__main__.test_obj object at 0x7fbf660d30b8>}
Append a
a= {'k1': '12345', 'k2': [0, 1, 2, 4], 'k3': <__main__.test_obj object at 0x7fbf660cdf98>}
b= {'k1': '12345', 'k2': [0, 1, 2], 'k3': <__main__.test_obj object at 0x7fbf660d30b8>}
Pop b:
a= {'k1': '12345', 'k2': [0, 1, 2, 4], 'k3': <__main__.test_obj object at 0x7fbf660cdf98>}
b= {'k1': '12345', 'k3': <__main__.test_obj object at 0x7fbf660d30b8>}
Modify object value:
values of a= {'obj_id': 'id1', 'text': '文本1', 'label': 'label1'}
values of b= {'obj_id': 'id1', 'text': '换文本了', 'label': 'label1'}

【更多、更及时内容欢迎留意微信公众号小窗幽记机器学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值