Harry Yu

深浅拷贝

Python
目录

深拷贝

神经网络结构复制

def clones(module, N):
    "自定义克隆函数,产生N个结构完全相同但是参数独立(deepcopy)的网络层"
    "Produce N identical layers."
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
class Decoder(nn.Module):
    "Generic N layer decoder with masking."
    def init(self, layer, N):
        super(Decoder, self).init()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)

pandas 数据处理

df_with_quant = df[has_quantitative].copy()

那么,Pandas 中的默认行为是 深拷贝 (Deep Copy)

  1. 默认参数: Pandas 的 copy() 方法原型是 DataFrame.copy(deep=True)。
    • 如果不传参数,默认 deep=True。
  2. 深拷贝 (deep=True)
    • 行为:它会复制数据(Data)和索引(Index)。
    • 结果:df_with_quant 拥有自己独立的内存空间。如果你修改 df_with_quant 中的数据,不会影响原始的 df。
    • 场景:这是绝大多数数据清洗步骤中想要的行为,可以安全地避免 SettingWithCopyWarning 警告。
  3. 浅拷贝 (deep=False)
    • 行为:只有当显式使用 .copy(deep=False) 时发生。它只复制对数据和索引的引用(Reference),不复制实际的数据内容。
    • 结果:新旧 DataFrame 共享同一块内存数据。修改其中一个可能会影响另一个(取决于具体的数据类型和修改方式)。

浅拷贝

A. 切片操作 [:] (针对列表)

list_1 = [1, [2, 3]]
list_2 = list_1[:]  # 浅拷贝

list_2[0] = 9
list_2[1][0] = 99

print(list_1)

这一句 list_2[0] = 9 做的事情叫做:引用重指向(Rebinding)

简单来说:它把 list_2 的第 0 个格子里的东西换掉了,而不是修改了原来的那个数字。

这正是为什么 list_1 不受影响的原因。我们需要区分”容器(List)“和”内容(Element)“。

详细图解

1. 初始状态(浅拷贝后)

执行 list_2 = list_1[:] 后,内存里有两个独立的列表外壳(List),但它们装着同样的内容。

  • list_1 的第0格贴着标签,指向内存里的数字 1。
  • list_2 的第0格贴着标签,也指向内存里的同一个数字 1。
list_1 [第0格] ────┐


                (数字对象 1)


list_2 [第0格] ────┘

2. 执行 list_2[0] = 9 时发生了什么?

这一行代码并不是去”把 1 变成 9”(因为整数在 Python 中是不可变的,你没法改它),而是做了两件事:

  1. 创建/找到新对象:在内存里找到(或创建)数字 9。
  2. 撕标签,贴新标签:把 list_2 第 0 格原本指向 1 的连线剪断,重新连向 9。

注意: 此时 list_1 的第 0 格没有任何人去动它,它依然指向 1。

list_1 [第0格] ──────────▶ (数字对象 1)  <-- list_1 依然指着这里


list_2 [第0格] ──X (剪断)

                 └───────▶ (数字对象 9)  <-- list_2 改指这里了

核心区别:为什么刚才那个 [1][0]=99 会影响?

这是初学者最容易晕的地方,请看这个对比:

情况 A(本题):修改外层索引 list_2[0] = 9

  • 操作对象:list_2 自己的格子
  • 动作:替换(把旧引用扔了,换个新的)。
  • 结果:因为 list_1 和 list_2 是两个独立的格子(浅拷贝保证了外壳独立),所以我换我家客厅的画,不影响你家客厅的画。

情况 B(上一题):修改内层索引 list_2[1][0] = 99

  • 操作对象:list_2 第 1 格所指向的那个公共列表
  • 动作:钻进去修改
  • 逻辑:
    1. 程序先读取 list_2[1],发现它指向一个列表 [2, 3]。
    2. 程序也知道 list_1[1] 指向同一个列表 [2, 3]。
    3. 代码指令是”进入这个列表,把里面的第一个数改掉”。
  • 结果:因为两人共用同一个内层列表,所以里面的东西变了,大家都看到了。

总结

  • list_2[0] = 9: 是**“换人”**。list_2 说:“我不想要这个 1 了,我要换成 9”。这只跟 list_2 有关。
  • list_2[1][0] = 99: 是**“改状态”**。list_2 说:“我要把我和 list_1 共同拥有的那个袋子里的苹果换成香蕉”。因为袋子是共享的,list_1 再打开看时,发现苹果变成了香蕉。

B. 工厂方法 .copy()

适用于列表(List)和字典(Dict)。

dict_1 = {'a': 1, 'b': [2, 3]}
dict_2 = dict_1.copy() # 浅拷贝

dict_2['b'][0] = 999
print(dict_1['b'][0]) # 输出 999

C. copy 模块的 copy()

这是通用的浅拷贝方法。

import copy
obj1 = [1, [2, 3]]
obj2 = copy.copy(obj1) # 浅拷贝

特别注意:赋值 = 不是浅拷贝!

初学者最容易混淆的地方:直接赋值连浅拷贝都不是,它只是引用(别名)。

df1 = pd.DataFrame(...)
df2 = df1  # 这不是拷贝!这是同一个对象贴了两个标签。
  • 赋值 (=):完全共享,连外壳都是同一个。
  • 浅拷贝:外壳不同(可以说是”换了层皮”),但肉(数据)是一样的。
  • 深拷贝:完全独立,你是你,我是我。