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

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,5000CU*H 3个月
实时数仓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'}

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

相关文章
|
23天前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
173 77
|
25天前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
116 62
|
20天前
|
存储 开发者 Python
Python 中的数据结构与其他编程语言数据结构的区别
不同编程语言都有其设计理念和应用场景,开发者需要根据具体需求和语言特点来选择合适的数据结构
|
21天前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
65 31
|
16天前
|
开发框架 .NET PHP
网站应用项目如何选择阿里云服务器实例规格+内存+CPU+带宽+操作系统等配置
对于使用阿里云服务器的搭建网站的用户来说,面对众多可选的实例规格和配置选项,我们应该如何做出最佳选择,以最大化业务效益并控制成本,成为大家比较关注的问题,如果实例、内存、CPU、带宽等配置选择不合适,可能会影响到自己业务在云服务器上的计算性能及后期运营状况,本文将详细解析企业在搭建网站应用项目时选购阿里云服务器应考虑的一些因素,以供参考。
|
19天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
48 1
|
21天前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
39 3
|
21天前
|
XML JSON API
如何使用Python将字典转换为XML
本文介绍了如何使用Python中的`xml.etree.ElementTree`库将字典数据结构转换为XML格式。通过定义递归函数处理字典到XML元素的转换,生成符合标准的XML文档,适用于与旧系统交互或需支持复杂文档结构的场景。示例代码展示了将一个简单字典转换为XML的具体实现过程。
15 1
|
25天前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
1月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install