目录

Python 序列解包与深浅拷贝

学习目标

  • 掌握序列解包的各种技巧
  • 理解可变对象与不可变对象的区别
  • 掌握深拷贝与浅拷贝的区别和使用场景
  • 理解 Python 的内存管理机制

一、序列解包

序列解包(Unpacking)是将可迭代对象中的元素分配给多个变量的语法。

1.1 基本解包

# 元组解包
coordinates = (10, 20)
x, y = coordinates
print(x, y)   # 10 20

# 列表解包
names = ["Alice", "Bob", "Charlie"]
a, b, c = names
print(a, b, c)   # Alice Bob Charlie

# 字符串解包
a, b, c = "Py3"
print(a, b, c)   # P y 3

1.2 扩展解包(Python 3+)

# 使用 * 捕获剩余元素
numbers = [1, 2, 3, 4, 5]
first, *rest = numbers
print(first)   # 1
print(rest)    # [2, 3, 4, 5]

# 中间捕获
first, *middle, last = numbers
print(first)    # 1
print(middle)   # [2, 3, 4]
print(last)     # 5

# 只取首尾
first, *_, last = numbers
print(first, last)   # 1 5

# 多个 *
# a, *b, *c = numbers   # SyntaxError: 只能有一个 *

1.3 解包的应用场景

# 交换变量
a, b = 10, 20
a, b = b, a
print(a, b)   # 20 10

# 遍历字典
user = {"name": "Alice", "age": 25, "city": "Beijing"}
for key, value in user.items():
    print(f"{key}: {value}")

# 遍历 enumerate
for index, (name, score) in enumerate([("Alice", 85), ("Bob", 92)]):
    print(f"{index}: {name} - {score}")

# 函数返回多个值
def get_stats(data):
    return min(data), max(data), sum(data) / len(data)

minimum, maximum, average = get_stats([1, 2, 3, 4, 5])
print(minimum, maximum, average)   # 1 5 3.0

# 忽略不需要的值
data = (1, 2, 3, 4, 5)
a, _, c, _, e = data
print(a, c, e)   # 1 3 5

1.4 解包函数参数

def greet(name, age, city):
    print(f"{name}, {age}岁, 来自{city}")

info = ["Alice", 25, "Beijing"]
greet(*info)   # Alice, 25岁, 来自Beijing

kwargs = {"name": "Bob", "age": 30, "city": "Shanghai"}
greet(**kwargs)   # Bob, 30岁, 来自Shanghai

# 混合使用
greet("Charlie", **{"age": 35, "city": "Shenzhen"})

二、可变对象与不可变对象

2.1 基本概念

类型 可变对象 不可变对象
数字 int, float, complex
序列 list str, tuple
映射 dict
集合 set frozenset
其他 bool, NoneType

2.2 可变对象的行为

# 列表是可变的
a = [1, 2, 3]
b = a           # b 引用同一个对象
b.append(4)
print(a)        # [1, 2, 3, 4]  a 也变了!
print(b)        # [1, 2, 3, 4]
print(a is b)   # True

2.3 不可变对象的行为

# 整数是不可变的
x = 10
y = x
y = 20
print(x)        # 10  x 不变
print(y)        # 20
print(x is y)   # False

# 字符串是不可变的
s1 = "hello"
s2 = s1
s2 = s2 + " world"
print(s1)       # hello  s1 不变
print(s2)       # hello world

2.4 函数参数传递

# 不可变对象:函数内修改不影响外部
def modify_int(x):
    x = 100
    print(f"函数内: {x}")

a = 10
modify_int(a)
print(f"函数外: {a}")   # 10(不变)

# 可变对象:函数内修改会影响外部
def modify_list(lst):
    lst.append(4)
    print(f"函数内: {lst}")

b = [1, 2, 3]
modify_list(b)
print(f"函数外: {b}")   # [1, 2, 3, 4](变了!)

# 避免修改:传递副本
def safe_modify(lst):
    lst = lst.copy()   # 创建副本
    lst.append(4)
    print(f"函数内: {lst}")

c = [1, 2, 3]
safe_modify(c)
print(f"函数外: {c}")   # [1, 2, 3](不变)

三、id() 与内存

# id() 返回对象的内存地址
a = [1, 2, 3]
b = a
print(id(a))   # 如: 140234567890
print(id(b))   # 相同的地址
print(a is b)  # True

# 创建新对象
c = [1, 2, 3]
print(id(c))   # 不同的地址
print(a is c)  # False
print(a == c)  # True(值相等)

# 小整数缓存
x = 100
y = 100
print(x is y)  # True(-5 到 256 之间会缓存)

m = 1000
n = 1000
print(m is n)  # False(大整数不缓存)

四、浅拷贝与深拷贝

4.1 赋值 vs 浅拷贝

import copy

# 赋值(引用)
a = [1, 2, [3, 4]]
b = a
print(a is b)   # True(同一对象)

# 浅拷贝(三种方式)
c = a.copy()        # 方法1
d = list(a)         # 方法2
e = a[:]            # 方法3
f = copy.copy(a)    # 方法4

print(a is c)       # False(不同对象)
print(a == c)       # True(值相等)

# 但浅拷贝只拷贝一层
a[2].append(5)
print(a)   # [1, 2, [3, 4, 5]]
print(c)   # [1, 2, [3, 4, 5]]  嵌套列表也变了!
print(a[2] is c[2])   # True(嵌套对象仍共享)

4.2 深拷贝

import copy

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

print(a is b)         # False
print(a[2] is b[2])   # False(嵌套对象也独立了!)

a[2].append(5)
print(a)   # [1, 2, [3, 4, 5]]
print(b)   # [1, 2, [3, 4]]  b 不受影响!

4.3 浅拷贝 vs 深拷贝对比

特性 浅拷贝 深拷贝
外层对象 新建 新建
嵌套可变对象 共享引用 递归新建
性能
内存 节省 占用多
适用场景 一层结构 多层嵌套结构

4.4 图解

原始对象 a = [1, 2, [3, 4]]

赋值 b = a:
  a ──→ [1, 2,  [3, 4]]
             ↑
  b ─────────┘

浅拷贝 c = a.copy():
  a ──→ [1, 2, ─┐
                ↓
  c ──→ [1, 2, ─┘→ [3, 4]]

深拷贝 d = copy.deepcopy(a):
  a ──→ [1, 2, ─→ [3, 4]]

  d ──→ [1, 2, ─→ [3, 4]]  (完全独立)

五、拷贝的陷阱

5.1 浅拷贝的意外

# 矩阵初始化陷阱
matrix = [[0] * 3] * 3
print(matrix)
# [[0, 0, 0], [0, 0, 0], [0, 0, 0]]

matrix[0][0] = 1
print(matrix)
# [[1, 0, 0], [1, 0, 0], [1, 0, 0]]  所有行都变了!

# 原因:三行引用同一个内部列表
print(matrix[0] is matrix[1])   # True

# 正确做法
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 1
print(matrix)
# [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

5.2 深拷贝的循环引用

a = [1, 2]
a.append(a)   # 循环引用:a 包含自己
print(a)      # [1, 2, [...]]

# 深拷贝可以处理循环引用
import copy
b = copy.deepcopy(a)
print(b)      # [1, 2, [...]]
print(b is b[2])   # True(保持循环引用结构)

六、综合示例

"""
数据备份工具:理解深浅拷贝的实际应用
"""
import copy

# 模拟一个复杂的数据结构
original_data = {
    "users": [
        {"name": "Alice", "settings": {"theme": "dark", "notifications": True}},
        {"name": "Bob", "settings": {"theme": "light", "notifications": False}},
    ],
    "config": {"version": "1.0", "debug": False}
}

# 1. 赋值(危险!)
reference = original_data
reference["config"]["debug"] = True
print("赋值后 original:", original_data["config"]["debug"])   # True(原数据被修改)

# 恢复
original_data["config"]["debug"] = False

# 2. 浅拷贝(部分安全)
shallow = copy.copy(original_data)
shallow["config"] = {"version": "2.0"}   # 修改外层对象,不影响原数据
print("浅拷贝修改外层后:", original_data["config"]["version"])   # 1.0(安全)

shallow["users"][0]["settings"]["theme"] = "blue"   # 修改嵌套对象
print("浅拷贝修改嵌套后:", original_data["users"][0]["settings"]["theme"])   # blue(危险!)

# 恢复
original_data["users"][0]["settings"]["theme"] = "dark"

# 3. 深拷贝(完全安全)
deep = copy.deepcopy(original_data)
deep["users"][0]["settings"]["theme"] = "red"
print("深拷贝修改后:", original_data["users"][0]["settings"]["theme"])   # dark(安全!)

小结

  • 序列解包使用 * 捕获剩余元素,可灵活分配变量
  • 可变对象(list、dict、set)赋值是引用传递,修改会影响原对象
  • 不可变对象(str、tuple、int)修改会创建新对象
  • 浅拷贝只复制一层,嵌套的可变对象仍共享引用
  • 深拷贝递归复制所有层级,完全独立但性能开销更大
  • 使用 copy.copy() 浅拷贝,copy.deepcopy() 深拷贝
  • 初始化嵌套列表时要避免 [[]] * n 的陷阱

练习

  1. 有一个列表 data = [1, 2, [3, 4], {"a": 5}],分别使用赋值、浅拷贝、深拷贝创建副本,然后修改嵌套列表和字典,观察原数据的变化。
  2. 编写一个函数 safe_append(lst, item),在不影响原列表的情况下返回新列表(新列表末尾添加了 item)。
  3. 解释以下代码的输出:
    a = [[]] * 3
    a[0].append(1)
    print(a)
  4. 使用序列解包,交换字典中的键值对:将 {"a": 1, "b": 2} 变成 {1: "a", 2: "b"}
  5. 创建一个 3x3 的二维列表并初始化为 0,确保每个子列表是独立的对象。