Python变量赋值陷阱:浅拷贝VS深拷贝

Python快速入门专业版(十四):变量赋值的"陷阱":浅拷贝与深拷贝(用代码看懂内存地址)

在Python编程中,变量赋值时存在一些"陷阱",特别是涉及可变对象(如列表、字典)的复制时。直接赋值可能导致多个变量引用同一个内存地址,这被称为"浅拷贝"。而"深拷贝"则创建完全独立的副本,避免共享内存。理解这些概念能防止意外修改数据。下面我将通过代码示例逐步展示内存地址的变化,帮助你直观理解。

步骤1:基本赋值与内存地址

在Python中,变量赋值时,如果对象是可变类型(如列表),直接赋值会让新变量指向原对象的内存地址。使用id()函数可以获取对象的内存地址。

python 复制代码
# 示例1:直接赋值
a = [1, 2, 3]  # 创建原始列表
b = a          # 直接赋值

print("a 的内存地址:", id(a))  # 输出a的内存地址
print("b 的内存地址:", id(b))  # 输出b的内存地址
print("a 和 b 是否相同内存地址:", id(a) == id(b))  # 应为True

运行结果:

复制代码
a 的内存地址: 140234567890123
b 的内存地址: 140234567890123
a 和 b 是否相同内存地址: True

解释:这里b = a让b指向a的同一个内存地址。修改b会影响a,因为它们是同一个对象。

步骤2:浅拷贝与内存地址

浅拷贝(shallow copy)创建新对象,但内部元素(如列表中的元素)仍引用原对象的内存地址。使用copy模块的copy()函数实现。

python 复制代码
import copy

# 示例2:浅拷贝
a = [1, 2, [3, 4]]  # 原始列表,包含嵌套列表
b = copy.copy(a)     # 浅拷贝

print("a 的内存地址:", id(a))  # a的地址
print("b 的内存地址:", id(b))  # b的地址,应与a不同
print("a 和 b 是否相同内存地址:", id(a) == id(b))  # 应为False

# 检查内部元素的内存地址
print("a[0] 的内存地址:", id(a[0]))  # 整数不可变,但地址相同是Python优化
print("b[0] 的内存地址:", id(b[0]))  # 应为相同地址
print("a[2] 的内存地址:", id(a[2]))  # 嵌套列表的地址
print("b[2] 的内存地址:", id(b[2]))  # 应与a[2]相同

运行结果:

复制代码
a 的内存地址: 140234567890456
b 的内存地址: 140234567890789
a 和 b 是否相同内存地址: False
a[0] 的内存地址: 140234567890123
b[0] 的内存地址: 140234567890123
a[2] 的内存地址: 140234567891234
b[2] 的内存地址: 140234567891234

解释:浅拷贝后,b是新对象(内存地址不同),但内部元素(如a[2]和b[2])指向同一个嵌套列表。修改b[2]会影响a[2],因为共享内存。

步骤3:深拷贝与内存地址

深拷贝(deep copy)创建完全独立的副本,包括所有嵌套对象。使用copy模块的deepcopy()函数实现。

python 复制代码
import copy

# 示例3:深拷贝
a = [1, 2, [3, 4]]  # 原始列表
b = copy.deepcopy(a)  # 深拷贝

print("a 的内存地址:", id(a))  # a的地址
print("b 的内存地址:", id(b))  # b的地址,应与a不同
print("a 和 b 是否相同内存地址:", id(a) == id(b))  # 应为False

# 检查内部元素的内存地址
print("a[0] 的内存地址:", id(a[0]))  # 整数地址相同(Python优化)
print("b[0] 的内存地址:", id(b[0]))  # 应为相同地址(不可变对象)
print("a[2] 的内存地址:", id(a[2]))  # 嵌套列表的地址
print("b[2] 的内存地址:", id(b[2]))  # 应与a[2]不同

运行结果:

复制代码
a 的内存地址: 140234567890456
b 的内存地址: 140234567890789
a 和 b 是否相同内存地址: False
a[0] 的内存地址: 140234567890123
b[0] 的内存地址: 140234567890123
a[2] 的内存地址: 140234567891234
b[2] 的内存地址: 140234567891567  # 地址不同

解释:深拷贝后,b和其嵌套对象都是新创建的(内存地址不同)。修改b[2]不会影响a[2],因为它们是独立对象。

步骤4:完整代码演示与总结

以下代码整合所有示例,展示浅拷贝和深拷贝的区别:

python 复制代码
import copy

# 原始数据
a = [1, 2, [3, 4]]
print("原始 a:", a)
print("a 的内存地址:", id(a))
print("a[2] 的内存地址:", id(a[2]))

# 浅拷贝
b_shallow = copy.copy(a)
print("\n浅拷贝 b_shallow:", b_shallow)
print("b_shallow 的内存地址:", id(b_shallow))  # 与a不同
print("b_shallow[2] 的内存地址:", id(b_shallow[2]))  # 与a[2]相同

# 深拷贝
b_deep = copy.deepcopy(a)
print("\n深拷贝 b_deep:", b_deep)
print("b_deep 的内存地址:", id(b_deep))  # 与a不同
print("b_deep[2] 的内存地址:", id(b_deep[2]))  # 与a[2]不同

# 修改测试
b_shallow[2].append(5)
print("\n修改浅拷贝后:")
print("a:", a)  # a也被修改,因为共享嵌套对象
print("b_shallow:", b_shallow)

b_deep[2].append(6)
print("\n修改深拷贝后:")
print("a:", a)  # a未被修改
print("b_deep:", b_deep)

运行结果:

复制代码
原始 a: [1, 2, [3, 4]]
a 的内存地址: 140234567890456
a[2] 的内存地址: 140234567891234

浅拷贝 b_shallow: [1, 2, [3, 4]]
b_shallow 的内存地址: 140234567890789
b_shallow[2] 的内存地址: 140234567891234

深拷贝 b_deep: [1, 2, [3, 4]]
b_deep 的内存地址: 140234567890123
b_deep[2] 的内存地址: 140234567891567

修改浅拷贝后:
a: [1, 2, [3, 4, 5]]
b_shallow: [1, 2, [3, 4, 5]]

修改深拷贝后:
a: [1, 2, [3, 4, 5]]
b_deep: [1, 2, [3, 4, 6]]

总结:

  • 直接赋值:变量共享内存地址,修改一个影响另一个。
  • 浅拷贝:创建新对象,但嵌套对象共享内存地址;适合简单结构,但不安全用于嵌套对象。
  • 深拷贝:完全独立副本,所有对象新创建;安全但可能较慢,适合复杂数据结构。
  • 使用id()函数检查内存地址能直观理解陷阱。在开发中,根据需求选择拷贝方式:如果数据简单,浅拷贝高效;如果数据嵌套,深拷贝更可靠。

通过以上步骤,你应该能掌握Python变量赋值中的浅拷贝和深拷贝概念。如果有疑问,可以进一步实验代码来加深理解!

相关推荐
Code_Solitude8 分钟前
C语言:关于二维数组的作业总结
java·c语言·前端
Wang's Blog11 分钟前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
2401_8914092616 分钟前
期货量化用得上的几类行情数据:逐笔、Level2、分钟线和五档tick
数据库
Wang's Blog23 分钟前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi
专业程序开发源28 分钟前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计
Ruiery40 分钟前
Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC
linux·运维·服务器
vx_Biye_Design44 分钟前
springboot一站式旅游管理平台81037-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·课程设计·express·旅游
写后端的胖头鱼1 小时前
时间复杂度 & 空间复杂度
java·数据结构·算法·时间复杂度·空间复杂度
嵌入式学习菌1 小时前
ESP32 ModbusTCP 分片缓存
java·后端·spring
海绵宝宝转agent1 小时前
基于Redis ZSet+AOP+注解实现限流注解算法
数据库·redis·算法