Day36 - 浅拷贝与深拷贝详解

详细讲解

1. 对象赋值与引用

在 Python 中,一切皆对象。赋值语句(如 a = b)并不复制对象,而是创建了对同一对象的引用。

# 赋值 - 同一个对象的不同引用
a = [1, 2, 3]
b = a

print(f"a: {a}")   # [1, 2, 3]
print(f"b: {b}")   # [1, 2, 3]
print(f"a is b: {a is b}")  # True - 同一对象

# 修改 a,b 也会变化
a.append(4)
print(f"修改后 a: {a}")   # [1, 2, 3, 4]
print(f"修改后 b: {b}")   # [1, 2, 3, 4] - b 也变了!

# id() 显示对象身份
print(f"id(a): {id(a)}")
print(f"id(b): {id(b)}")  # 相同

2. 浅拷贝(Shallow Copy)

浅拷贝创建一个新对象,但只复制原对象中的第一层引用。对于嵌套对象,只复制其引用,不复制嵌套对象本身。

import copy

# 浅拷贝的几种方式
original = [1, 2, [3, 4]]

# 方式1: copy.copy()
shallow1 = copy.copy(original)

# 方式2: list() / dict() / 切片等
shallow2 = list(original)

# 方式3: 切片
shallow3 = original[:]

# 方式4: dict(original)
original_dict = {'a': 1, 'b': [1, 2]}
shallow_dict = dict(original_dict)

print(f"original: {original}")
print(f"shallow: {shallow1}")
print(f"original is shallow: {original is shallow1}")  # False - 新对象
print(f"original[2] is shallow[2]: {original[2] is shallow1[2]}")  # True - 内部对象相同

# 修改第一层,不影响原对象
shallow1[0] = 999
print(f"修改后 original: {original}")  # [1, 2, [3, 4]] - 不变
print(f"修改后 shallow: {shallow1}")   # [999, 2, [3, 4]]

# 修改深层对象,影响原对象
shallow1[2].append(5)
print(f"修改深层后 original: {original}")  # [1, 2, [3, 4, 5]] - 变了!
print(f"修改深层后 shallow: {shallow1}")   # [999, 2, [3, 4, 5]]

3. 深拷贝(Deep Copy)

深拷贝递归地复制所有嵌套对象,创建完全独立的对象副本。

import copy

original = [1, 2, [3, 4]]

# 深拷贝
deep = copy.deepcopy(original)

print(f"original: {original}")
print(f"deep: {deep}")
print(f"original is deep: {original is deep}")  # False
print(f"original[2] is deep[2]: {original[2] is deep[2]}")  # False - 完全独立!

# 修改第一层,不影响原对象
deep[0] = 999
print(f"修改后 original: {original}")  # [1, 2, [3, 4]] - 不变
print(f"修改后 deep: {deep}")   # [999, 2, [3, 4]]

# 修改深层对象,也不影响原对象
deep[2].append(5)
print(f"修改深层后 original: {original}")  # [1, 2, [3, 4]] - 不变!
print(f"修改深层后 deep: {deep}")   # [999, 2, [3, 4, 5]]

4. 拷贝操作对比表

┌─────────────────────────────────────────────────────────────┐ │ 拷贝方式对比 │ ├─────────────┬───────────────────────────────────────────────┤ │ 赋值 (=) │ 共享引用,修改互相影响 │ ├─────────────┼───────────────────────────────────────────────┤ │ 浅拷贝 │ 第一层独立,深层共享引用 │ ├─────────────┼───────────────────────────────────────────────┤ │ 深拷贝 │ 完全独立,所有层级都复制 │ └─────────────┴───────────────────────────────────────────────┘

5. 不可变对象与拷贝

重要:对于不可变对象(如 tuple, str, int, frozenset),由于其不可修改,拷贝的意义不大,Python 可能会复用对象。

import copy

# 不可变对象
a = "hello"
b = copy.copy(a)
c = copy.deepcopy(a)

print(f"a: {a}, b: {b}, c: {c}")
print(f"a is b: {a is b}")  # True - 不可变对象可能复用
print(f"a is c: {a is c}")  # True - 不可变对象可能复用

# tuple 整体不可变,但内部可变对象呢?
t = (1, 2, [3, 4])
t_copy = copy.copy(t)
t_deep = copy.deepcopy(t)

print(f"t is t_copy: {t is t_copy}")  # True - tuple 不可变,copy 可能复用
print(f"t is t_deep: {t is t_deep}")  # False - deepcopy 总是创建新对象

# 但内部的 list 是独立的
t_copy[2].append(5)
print(f"t: {t}")      # (1, 2, [3, 4]) - tuple 不变
print(f"t_copy: {t_copy}")  # (1, 2, [3, 4, 5])

6. 浅拷贝的常见方式

# 1. 切片拷贝
list1 = [1, 2, 3]
list2 = list1[:]

# 2. list() / dict() / set()
list3 = list(list1)
dict1 = {'a': 1}
dict2 = dict(dict1)
set1 = {1, 2}
set2 = set(set1)

# 3. list.copy() / dict.copy()
list4 = list1.copy()
dict3 = dict1.copy()

# 4. copy.copy()
import copy
list5 = copy.copy(list1)

# 5. copy.copy() for dict with nested
original = {'a': [1, 2, 3], 'b': {'x': 1}}
shallow = original.copy()

7. 深拷贝的常见方式

import copy

# 1. copy.deepcopy()
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)

# 2. 递归自定义深拷贝
def deep_copy(obj):
    if isinstance(obj, (list, tuple)):
        return type(obj)(deep_copy(item) for item in obj)
    elif isinstance(obj, dict):
        return {key: deep_copy(val) for key, val in obj.items()}
    elif isinstance(obj, (int, str, float, bool, type(None))):
        return obj
    else:
        # 其他对象尝试使用 __dict__ 复制
        try:
            result = object.__new__(type(obj))
            result.__dict__.update(obj.__dict__)
            return result
        except:
            return obj

8. 拷贝的注意事项

8.1 循环引用

import copy

# 循环引用的对象
list1 = [1, 2]
list2 = [3, 4]
list1.append(list2)
list2.append(list1)  # list1 -> list2 -> list1

# deepcopy 能处理循环引用,不会无限递归
deep = copy.deepcopy(list1)
print(f"深拷贝完成,无无限递归")

8.2 对象自定义拷贝行为

import copy

class MyClass:
    def __init__(self, value):
        self.value = value
        self.data = [1, 2, 3]
    
    # 定义浅拷贝行为
    def __copy__(self):
        new_obj = MyClass(self.value)
        new_obj.data = list(self.data)  # 重新创建列表
        return new_obj
    
    # 定义深拷贝行为
    def __deepcopy__(self, memo):
        new_obj = MyClass(self.value)
        memo[id(self)] = new_obj  # 防止循环引用
        new_obj.data = copy.deepcopy(self.data, memo)
        return new_obj

obj = MyClass(42)
obj_copy = copy.copy(obj)   # 调用 __copy__
obj_deep = copy.deepcopy(obj)  # 调用 __deepcopy__

8.3 单例对象

import copy

# 单例对象(如 None, True, False)
singleton = None
copy_of_singleton = copy.copy(singleton)
deep_copy_of_singleton = copy.deepcopy(singleton)

print(f"singleton is copy: {singleton is copy_of_singleton}")  # True

# 深拷贝也保持单例特性
print(f"singleton is deep: {singleton is deep_copy_of_singleton}")  # True

9. 实战案例:配置对象拷贝

import copy
from dataclasses import dataclass, field

@dataclass
class DatabaseConfig:
    """数据库配置"""
    host: str = 'localhost'
    port: int = 3306
    name: str = 'testdb'
    credentials: dict = field(default_factory=dict)

@dataclass
class AppConfig:
    """应用配置"""
    name: str = 'MyApp'
    debug: bool = False
    db: DatabaseConfig = field(default_factory=DatabaseConfig)
    
    def create_copy(self):
        """创建深拷贝"""
        return copy.deepcopy(self)
    
    def create_shallow(self):
        """创建浅拷贝"""
        return copy.copy(self)

# 创建原始配置
config1 = AppConfig(
    name='Production',
    debug=False,
    db=DatabaseConfig(
        host='db.example.com',
        credentials={'user': 'admin'}
    )
)

# 深拷贝 - 完全独立
config2 = config1.create_copy()
config2.name = 'Development'
config2.db.host = 'localhost'  # 不影响 config1

print(f"原始配置: {config1.name}, {config1.db.host}")
print(f"拷贝配置: {config2.name}, {config2.db.host}")
# 输出:
# 原始配置: Production, db.example.com
# 拷贝配置: Development, localhost

# 浅拷贝 - 嵌套对象共享引用
config3 = config1.create_shallow()
config3.name = 'Testing'
config3.db.host = '127.0.0.1'  # 这会影响到 config1!

print(f"原始配置: {config1.name}, {config1.db.host}")
print(f"浅拷贝配置: {config3.name}, {config3.db.host}")
# 输出:
# 原始配置: Production, 127.0.0.1  <- 被影响了!
# 浅拷贝配置: Testing, 127.0.0.1

10. 拷贝判断方法

import copy

a = [1, 2, 3]
b = a
c = copy.copy(a)
d = copy.deepcopy(a)

print(f"a is b: {a is b}")      # True - 同一引用
print(f"a is c: {a is c}")      # False - 浅拷贝
print(f"a is d: {a is d}")      # False - 深拷贝

# 检查值是否相等
print(f"a == b: {a == b}")      # True
print(f"a == c: {a == c}")      # True
print(f"a == d: {a == d}")      # True

背诵版

三种拷贝方式速查

┌─────────────────────────────────────────────────────────────┐ │ 方式 │ 第一层 │ 深层(嵌套) │ 说明 │ ├───────────┼──────────┼────────────────┼────────────────────┤ │ 赋值 = │ 共享 │ 共享 │ 同一引用 │ ├───────────┼──────────┼────────────────┼────────────────────┤ │ 浅拷贝 │ 独立 │ 共享 │ copy.copy() │ ├───────────┼──────────┼────────────────┼────────────────────┤ │ 深拷贝 │ 独立 │ 独立 │ copy.deepcopy() │ └───────────┴──────────┴────────────────┴────────────────────┘

拷贝方法总结

方法 类型 说明
a = b 赋值 共享引用
a[:] 浅拷贝 切片,适用于 list
list(a) 浅拷贝 构造函数
a.copy() 浅拷贝 方法形式
copy.copy(a) 浅拷贝 模块函数
copy.deepcopy(a) 深拷贝 完全独立副本

考前记忆

面试重点

  1. 赋值 vs 拷贝的区别

    • 赋值:创建新引用,指向同一对象
    • 拷贝:创建新对象
  2. 浅拷贝只复制第一层

    • 第一层是独立的
    • 深层嵌套对象仍是引用
  3. 深拷贝完全独立

    • 所有层级都递归复制
    • 循环引用也能正确处理
  4. 不可变对象的特点

    • int, str, tuple 等不需要拷贝
    • 拷贝可能复用对象
  5. __copy____deepcopy__ 方法

    • 自定义类可定义这两个方法控制拷贝行为

记忆口诀

赋值共享同一对象, 浅拷贝第一层独立。 深拷贝完全递归复制, immutable无需拷贝。

测试题

选择题

1. 下面代码的输出是什么?

import copy
a = [1, [2, 3]]
b = copy.copy(a)
b[0] = 99
b[1].append(4)
print(a)
# A. [1, [2, 3]]
# B. [99, [2, 3]]
# C. [1, [2, 3, 4]]
# D. [99, [2, 3, 4]]

答案:C(第一层独立,深层共享)


2. 深拷贝的特点是什么?

# A. 只复制第一层
# B. 创建新引用
# C. 递归复制所有嵌套对象
# D. 不复制不可变对象

答案:C


3. list1 = list2[:] 是什么类型的拷贝?

# A. 赋值
# B. 浅拷贝
# C. 深拷贝
# D. 都不是

答案:B


4. 下面哪个是深拷贝的正确方式?

# A. a = b
# B. a = copy.copy(b)
# C. a = copy.deepcopy(b)
# D. a = b[:]

答案:C


5. 不可变对象(int, str, tuple)拷贝时会怎样?

# A. 总是创建新对象
# B. 可能复用已有对象
# C. 报错
# D. 返回 None

答案:B(不可变对象拷贝可能复用,Python 优化)


编程题

1. 实现一个安全的配置合并函数:

import copy

def merge_configs(base, override):
    """
    合并配置,override 的值会覆盖 base
    使用深拷贝确保不修改原始配置
    """
    result = copy.deepcopy(base)
    
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = merge_configs(result[key], value)
        else:
            result[key] = copy.deepcopy(value)
    
    return result

# 测试
base_config = {
    'database': {
        'host': 'localhost',
        'port': 3306,
        'options': {'timeout': 30}
    },
    'app': {'debug': False}
}

override_config = {
    'database': {
        'host': 'production.db.com',
        'options': {'timeout': 60}
    },
    'app': {'debug': True}
}

merged = merge_configs(base_config, override_config)
print(merged)
# {'database': {'host': 'production.db.com', 'port': 3306, 'options': {'timeout': 60}},
#  'app': {'debug': True}}

# 验证原始配置未被修改
print(base_config['database']['host'])  # 'localhost'

2. 实现一个不可变配置类:

import copy
from typing import Any

class ImmutableConfig:
    """不可变配置类"""
    
    def __init__(self, data):
        self._data = self._deep_freeze(data)
    
    @staticmethod
    def _deep_freeze(obj):
        """深度冻结 - 将所有嵌套 dict 转为 ImmutableConfig"""
        if isinstance(obj, dict):
            return ImmutableConfig(obj._data if isinstance(obj, ImmutableConfig) else obj)
        elif isinstance(obj, list):
            return tuple(ImmutableConfig._deep_freeze(item) for item in obj)
        else:
            return obj
    
    def get(self, key, default=None):
        """获取值"""
        keys = key.split('.')
        value = self._data
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
            elif isinstance(value, ImmutableConfig):
                value = value._data.get(k)
            else:
                return default
            if value is None:
                return default
        return value
    
    def with_overrides(self, **kwargs):
        """返回带覆盖的新实例(原实例不变)"""
        new_data = copy.deepcopy(self._data)
        for key, value in kwargs.items():
            new_data[key] = value
        return ImmutableConfig(new_data)
    
    def __repr__(self):
        return f"ImmutableConfig({self._data})"

# 使用
config = ImmutableConfig({
    'host': 'localhost',
    'port': 8080,
    'database': {
        'name': 'testdb',
        'user': 'admin'
    }
})

# 获取值
print(config.get('host'))           # localhost
print(config.get('database.name'))  # testdb

# 创建新配置(不修改原配置)
new_config = config.with_overrides(host='production', port=443)
print(config.get('host'))    # localhost - 原配置不变
print(new_config.get('host'))  # production

3. 检测拷贝类型的装饰器:

import copy

def verify_copy(original, copied, expected_type):
    """验证拷贝结果的正确性"""
    checks = {
        'assignment': lambda o, c: o is c,
        'shallow': lambda o, c: o is not c and o[2] is c[2],  # 假设有嵌套
        'deep': lambda o, c: o is not c and (len(o) != len(c) or o[2] is not c[2])
    }
    
    return checks.get(expected_type, lambda *a: False)(original, copied)

# 测试
nested = [1, [2, 3]]
assignment = nested
shallow = copy.copy(nested)
deep = copy.deepcopy(nested)

print(f"Assignment: {nested is assignment}")  # True
print(f"Shallow: {nested is not shallow and nested[1] is shallow[1]}")  # True
print(f"Deep: {nested is not deep and nested[1] is not deep[1]}")  # True

问答题

Q1: 什么是浅拷贝?什么是深拷贝?请举例说明它们的区别。

  • 浅拷贝:创建一个新对象,但只复制原对象的第一层。对于嵌套的可变对象(如 list 中的 list),只复制引用,不复制嵌套对象本身。
  • 深拷贝:递归地复制所有嵌套对象,创建完全独立的对象副本。
import copy
original = [1, [2, 3]]

shallow = copy.copy(original)
shallow[1].append(4)  # original 也会变化

deep = copy.deepcopy(original)
deep[1].append(5)  # original 不受影响

Q2: 什么情况下需要使用深拷贝而不是浅拷贝?

需要深拷贝的场景:

  1. 修改副本时不想影响原对象
  2. 原对象包含多层嵌套的可变对象
  3. 需要对配置、状态等进行隔离修改
  4. 多线程或并发场景下需要独立状态

典型场景:

  • 配置文件修改
  • 游戏对象克隆
  • 数据处理中的数据隔离
  • 状态机中的状态副本

Q3: Python 中哪些对象是可变的?哪些是不可变的?

可变对象

  • list, dict, set
  • bytearray
  • 用户自定义类(默认)

不可变对象

  • int, float, bool
  • str, bytes
  • tuple, frozenset
  • None
  • complex, decimal

重要:包含可变对象的不可变对象(如包含 list 的 tuple)修改内部可变对象时,原对象也会变化。


参考资料