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

简介: 本文主要介绍字典内存释放及其浅拷贝和深拷贝之间的区别

背景

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

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

相关文章
|
11月前
|
监控 数据可视化 数据挖掘
Python Rich库使用指南:打造更美观的命令行应用
Rich库是Python的终端美化利器,支持彩色文本、智能表格、动态进度条和语法高亮,大幅提升命令行应用的可视化效果与用户体验。
1002 0
|
数据采集 监控 Java
Python 函数式编程的执行效率:实际应用中的权衡
Python 函数式编程的执行效率:实际应用中的权衡
463 102
|
存储 数据可视化 BI
Python可视化应用——学生成绩分布柱状图展示
本程序使用Python读取Excel中的学生成绩数据,统计各分数段人数,并通过Matplotlib库绘制柱状图展示成绩分布。同时计算最高分、最低分及平均分,实现成绩可视化分析。
958 0
|
10月前
|
存储 Java 索引
(Python基础)新时代语言!一起学习Python吧!(二):字符编码由来;Python字符串、字符串格式化;list集合和tuple元组区别
字符编码 我们要清楚,计算机最开始的表达都是由二进制而来 我们要想通过二进制来表示我们熟知的字符看看以下的变化 例如: 1 的二进制编码为 0000 0001 我们通过A这个字符,让其在计算机内部存储(现如今,A 字符在地址通常表示为65) 现在拿A举例: 在计算机内部 A字符,它本身表示为 65这个数,在计算机底层会转为二进制码 也意味着A字符在底层表示为 1000001 通过这样的字符表示进行转换,逐步发展为拥有127个字符的编码存储到计算机中,这个编码表也被称为ASCII编码。 但随时代变迁,ASCII编码逐渐暴露短板,全球有上百种语言,光是ASCII编码并不能够满足需求
414 4
|
11月前
|
存储 大数据 Unix
Python生成器 vs 迭代器:从内存到代码的深度解析
在Python中,处理大数据或无限序列时,迭代器与生成器可避免内存溢出。迭代器通过`__iter__`和`__next__`手动实现,控制灵活;生成器用`yield`自动实现,代码简洁、内存高效。生成器适合大文件读取、惰性计算等场景,是性能优化的关键工具。
492 2
|
11月前
|
机器学习/深度学习 算法 安全
【强化学习应用(八)】基于Q-learning的无人机物流路径规划研究(Python代码实现)
【强化学习应用(八)】基于Q-learning的无人机物流路径规划研究(Python代码实现)
771 6
|
传感器 数据采集 监控
Python生成器与迭代器:从内存优化到协程调度的深度实践
简介:本文深入解析Python迭代器与生成器的原理及应用,涵盖内存优化技巧、底层协议实现、生成器通信机制及异步编程场景。通过实例讲解如何高效处理大文件、构建数据流水线,并对比不同迭代方式的性能特点,助你编写低内存、高效率的Python代码。
437 0
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
656 0
|
11月前
|
设计模式 缓存 运维
Python装饰器实战场景解析:从原理到应用的10个经典案例
Python装饰器是函数式编程的精华,通过10个实战场景,从日志记录、权限验证到插件系统,全面解析其应用。掌握装饰器,让代码更优雅、灵活,提升开发效率。
675 0
|
存储 监控 安全
企业上网监控系统中红黑树数据结构的 Python 算法实现与应用研究
企业上网监控系统需高效处理海量数据,传统数据结构存在性能瓶颈。红黑树通过自平衡机制,确保查找、插入、删除操作的时间复杂度稳定在 O(log n),适用于网络记录存储、设备信息维护及安全事件排序等场景。本文分析红黑树的理论基础、应用场景及 Python 实现,并探讨其在企业监控系统中的实践价值,提升系统性能与稳定性。
813 1

推荐镜像

更多