广告
您当前的位置: 首页 >  技术 >  Python

Python 中的深浅拷贝 (Copy/Deepcopy) 底层原理与对象复用优化深度剖析

作者:CoderWang 时间:2026-06-24 阅读数:3人阅读

在 Python 的日常编程中,我们经常需要处理对象的复制、传递和比较。然而,由于 Python 独特的“一切皆对象,变量皆指针”的底层内存设计,许多开发者在面对对象赋值、拷贝操作时,往往会因为搞不清浅拷贝(Shallow Copy)深拷贝(Deep Copy) 的区别,而踩入意想不到的“数据污染”深坑。

此外,为了追求内存和性能的极致平衡,Python(主要是官方的 CPython 实现)在后台默默运行着一套对象复用机制(如整数缓存池、字符串驻留)

本文将从底层的 C 语言指针和内存分配视角,深度探秘 Python 对象的拷贝原理解析与复用调优。


一、 终极辩证:值相等(==)与身份相等(is)

要理解拷贝,必须首先理解 Python 对“相同”的定义:

  • ==(值相等):比较两个对象所包含的数据值是否相同。在底层,这触发的是对象的 __eq__() 特殊方法。
  • is(身份/内存地址相等):比较两个对象在内存中的地址是否完全一致(即是否指向同一个指针)。在底层,这等同于 id(a) == id(b)
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # 输出: True (值相同)
print(a is b)  # 输出: False (内存地址不同,这是两个独立的列表对象)

二、 CPython 对象的隐式复用:缓存与驻留

在很多时候,即使你显式创建了两个不同的变量,Python 底层为了节约内存,也可能会让它们共享同一个对象:

1. 小整数对象池(Integer Cache)

为了避免频繁地分配和回收小整数对象,CPython 在启动时就会在内存中预先创建好一个数组,存放了从 -5256 的所有整数对象。 * 在此范围内的所有小整数,全局都只有一个实例。

x = 256
y = 256
print(x is y)  # 输出: True (命中缓存池,地址完全一样)

x = 257
y = 257
print(x is y)  # 输出: False (超出范围,重新分配了不同的内存地址)

2. 字符串驻留机制(String Interning)

为了提升字典 key 的查找效率,CPython 内部维护了一个驻留字符串字典。 * 规则:通常,只包含字母、数字和下划线且长度较短的字符串,在创建时会被自动“驻留”(Interned)。重复声明相同内容的字符串会直接引用已有实例。 * 手动控制:我们可以使用 sys.intern(string) 强行在运行时将一个字符串加入驻留空间,这在处理数百万个重复文本数据时,能暴跌 90% 的内存开销

import sys
# 强制驻留
a = sys.intern("hello world info!")
b = sys.intern("hello world info!")
print(a is b)  # 输出: True (即使包含空格,手动驻留后也指向同一个地址)

三、 浅拷贝与深拷贝的数据流动差异

当我们真正需要复制一个非基本类型对象时,copy 模块提供了两种手段:

1. 浅拷贝(copy.copy()

  • 数据行为:创建一个新对象,但新对象内部的子元素指针,依旧直接指向原对象中子元素的内存地址
  • 致命陷阱:对于嵌套的复合对象(如列表中还套着列表),修改子列表中的元素会同时污染原对象!
import copy

origin = [[1, 2], 3]
shallow = copy.copy(origin)

# 修改非嵌套元素,无影响
shallow[1] = 4
# 修改嵌套子对象,灾难发生!
shallow[0].append(99)

print("Origin:", origin)   # 输出: Origin: [[1, 2, 99], 3] (原对象也被污染了!)
print("Shallow:", shallow) # 输出: Shallow: [[1, 2, 99], 4]

2. 深拷贝(copy.deepcopy()

  • 数据行为:不仅复制最外层的容器对象,还会递归地复制其内部所有级别的嵌套子对象,完全断开新老对象之间的一切内存联系。
origin = [[1, 2], 3]
deep = copy.deepcopy(origin)

deep[0].append(99)
print("Origin:", origin) # 输出: Origin: [[1, 2], 3] (原对象安然无恙!)

3. deepcopy 底层如何预防循环引用死循环?

如果深拷贝遇到循环引用的对象(例如 A 引用 B,B 又引用 A),递归复制岂不是会无限陷入死循环导致栈溢出? * 底层原理解析copy.deepcopy 在内部维护了一个名为 memo(备忘录) 的字典。 * 每当开始复制一个对象时,deepcopy 会首先将其原始内存 id 记录进 memo 字典中:memo[id(obj)] = copy_obj。 * 在递归子元素时,如果发现某个子元素的 id 已经在 memo 字典中,则直接返回已复制好的实例,优雅地斩断了无限循环环路。


四、 高级自定性:重写拷贝协议

有时在设计复杂的底层框架时,某些特定的物理句柄(如数据库连接、Socket套接字)是绝对不能被复制的,或者复制时需要执行特殊的重组逻辑。 我们可以在自定义类中,通过重写 __copy__()__deepcopy__() 方法来实现精密控制:

import copy

class SecureResource:
    def __init__(self, name, secret_key):
        self.name = name
        self.secret_key = secret_key

    def __copy__(self):
        # 浅拷贝时,脱敏敏感密钥
        return SecureResource(self.name, "MASKED")

    def __deepcopy__(self, memo):
        # 深拷贝时,生成全新的密钥副本
        new_key = f"DEEP_COPY_{self.secret_key}"
        # 注意:深拷贝方法必须接受 memo 字典参数,并将其传递给嵌套拷贝
        return SecureResource(copy.deepcopy(self.name, memo), new_key)

res = SecureResource("DB_CONN", "123456")
shallow_res = copy.copy(res)
deep_res = copy.deepcopy(res)

print("Shallow key:", shallow_res.secret_key) # 输出: MASKED
print("Deep key:", deep_res.secret_key)       # 输出: DEEP_COPY_123456

五、 性能调优避坑原则:不要滥用 deepcopy

深拷贝非常安全,但它是一件极其沉重的高开销武器。 * 性能瓶颈:因为 deepcopy 底层需要使用大量的 Python 级反射、频繁遍历 MRO 继承链、并且对每个子对象都要维护和查询 memo 字典。这导致 deepcopy 的耗时通常是普通对象分配的几十倍甚至上百倍。 * 优化方法: * 在编写性能敏感的高频业务代码时,尽量通过手动组装新对象(如 new_obj = Point(old.x, old.y))来代替直接调用 copy.deepcopy。 * 如果需要复制大批量的纯数据,考虑将数据转换为 dict,使用 dict(original) 或者是列表推导式进行层级浅拷贝,其运行速率会呈量级提升。

总结

值比较 == 与指针比对 is 划清了 Python 数据与物理地址的边界,而整数小池与字符串驻留则印证了 CPython 对重复垃圾碎片的极致回收节约。浅拷贝与深拷贝在嵌套元素指针转移上的抉择,区分了共享实体与完全隔离,其底层的 memo 字典更是解决循环引用自适应的精妙设计。在日常系统架构中,根据数据结构合理抉择拷贝类型,防范敏感信息复制泄露,并规避 deepcopy 带来的深层反射性能损耗,是编写流畅高性能 Python 代码的基础功底。

本站所有文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。

评论交流 (0)

正在加载评论...
头像

CoderWang

当你还撑不起你的梦想时,就要去奋斗。如果缘分安排我们相遇,请不要让她擦肩和过。我们一起奋斗!

微信