Python应用专题 | 8:字典内存释放及其浅拷贝和深拷贝之间的区别

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 本文主要介绍字典内存释放及其浅拷贝和深拷贝之间的区别

背景

在用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'}

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

相关文章
|
21天前
|
机器学习/深度学习 缓存 Linux
python环境学习:pip介绍,pip 和 conda的区别和联系。哪个更好使用?pip创建虚拟环境并解释venv模块,pip的常用命令,conda的常用命令。
本文介绍了Python的包管理工具pip和环境管理器conda的区别与联系。pip主要用于安装和管理Python包,而conda不仅管理Python包,还能管理其他语言的包,并提供强大的环境管理功能。文章还讨论了pip创建虚拟环境的方法,以及pip和conda的常用命令。作者推荐使用conda安装科学计算和数据分析包,而pip则用于安装无法通过conda获取的包。
41 0
|
25天前
|
存储 安全 Serverless
Python学习四:流程控制语句(if-else、while、for),高级数据类型(字符串、列表、元组、字典)的操作
这篇文章主要介绍了Python中的流程控制语句(包括if-else、while、for循环)和高级数据类型(字符串、列表、元组、字典)的操作。
28 0
|
28天前
|
存储 自然语言处理 数据库
Python字典操作实现文章敏感词检索
Python字典操作实现文章敏感词检索
|
28天前
|
存储 JSON 数据处理
分析、总结Python使用列表、元组、字典的场景
分析、总结Python使用列表、元组、字典的场景
|
1月前
|
存储 Java Serverless
【Python】字典
【Python】字典
23 0
|
1月前
|
Python
Python操作:字符串--列表--元组--字典--运算符 (一)
Python操作:字符串--列表--元组--字典--运算符 (一)
|
1月前
|
Python
Python操作:字符串--列表--元组--字典--运算符 (二)
Python操作:字符串--列表--元组--字典--运算符 (二)
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
325 0
|
10天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
19 1
|
14天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。