当前位置: 首页 > article >正文

Python自学 - 引用与拷贝探索(防坑关键知识)

1 Python自学 - 引用与拷贝探索(防坑关键知识)

1.1 概述

  读者如果学习过C/C++语言,对引用的概念应该会比较了解,引用本质是将地址共享给另一个变量,两个变量指向同一个内存地址,任何一个变量的修改都会在另一个变量上即时生效(实际都是一个内存)。

1.2 引用探索

1.2.1 简单结构是否存在引用

  • 结论
    • 简单数据结构:整型、浮点型、字符串,在变量给变量赋值时存在共享内存现象,但变量修改后即分裂成两个完全不同的对象。
    • 元组:行为与简单变量相同。
    • 复杂数据结构:列表、字典、集合,变量对变量赋值时共享内存,对任一变量使用成员函数进行修改,所有变量都同步修改(同一地址),但对变量使用同类常量对象赋值时,变量分裂成完全新的变量。
1.2.1.1 整型:变量赋值存在引用,修改后分裂成不同对象
  • 整型变量赋值给另一个变量后,两个变量的值、地址都相同,表示这里存在引用。
var1 = 10
var2 = var1  #将变量var1赋值给变量var2
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 10, addr2:140732679062728
  • 修改变量值后,完全分裂成两个不同的对象
var1 = 10
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 10, addr2:140732679062728
var2 = 30 #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10, addr1:140732679062728, var2: 30, addr2:140732679063368
1.2.1.2 浮点型:变量赋值存在引用,修改后分裂成不同对象

  浮点型变量的情况与整型相同,变量赋值时共享内存地址。

var1 = 10.011
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10.011, addr1:2038061764976, var2: 10.011, addr2:2038061764976
var2 = 30.67 #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: 10.011, addr1:2038061764976, var2: 30.67, addr2:2038060456432
1.2.1.3 字符串:变量赋值存在引用,修改后分裂成不同对象

  字符串变量情况与整型相同,变量赋值时共享内存地址

var1 = "python"
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: python, addr1:2733226584192, var2: python, addr2:2733226584192
var2 = "javascript" #变量2的值修改后,得到了新的地址,与变量1分裂成两个不同的对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: python, addr1:2733226584192, var2: javascript, addr2:2733228213936

1.2.2 复杂结构的引用

  复杂结构研究对象为:列表、元组、字典、集合。

1.2.2.1 列表:两个变量的赋值是共享地址,任一变量修改都会同时生效

  列表变量使用另一列表变量赋值后,两个列表变量指向同一地址,且任何一个变量对列表的修改都会影响到两个列表变量(限于成员函数修改),如果使用列表对象赋值,则两个变量会分裂

  • 使用成员函数修改变量,两个变量都会同步更新(实际是一个地址)
var1 = [1, 2, 3]
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2086805069888, var2: [1, 2, 3], addr2:2086805069888
var2.extend([4, 5]) #变量2的值修改后,地址没有变化,且查看变量1和变更2时,两个列表的数据都变化且值相同
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3, 4, 5], addr1:2086805069888, var2: [1, 2, 3, 4, 5], addr2:2086805069888
  • 使用初始化赋值后,两个变量会分裂成两个不同对象
var1 = [1, 2, 3]
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2363003985984, var2: [1, 2, 3], addr2:2363003985984
var2 = [1, 2, 3, 4, 5] #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: [1, 2, 3], addr1:2363003985984, var2: [1, 2, 3, 4, 5], addr2:2363005524544
1.2.2.2 元组:两个变量赋值时共享地址,修改后分裂为不同对象

  由于元组的成员是无法变化的,所以元组的行为和简单数据类型相同。

var1 = (1, 2, 3)
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: (1, 2, 3), addr1:1219793071552, var2: (1, 2, 3), addr2:1219793071552
var2 = (1, 2, 3, 4, 5) #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: (1, 2, 3), addr1:1219793071552, var2: (1, 2, 3, 4, 5), addr2:1219793104032
1.2.2.3 字典:两个变量的赋值是共享地址,任一变量修改都会同时生效

  字典的行为与列表相同,两个变量赋值时共享地址,指向同一个地址,修改一个变量,另一个变量也同步生效(成员函数),但使用字典对象赋值后,变量分裂成完全新的变量

var1 = {"key1":1, "key2":2}
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2}, addr1:2513141032320, var2: {'key1': 1, 'key2': 2}, addr2:2513141032320
var2.update(x=1, y=2) #变量2更新后,地址不变,两个变量内容完全相同
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr1:2513141032320, var2: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr2:2513141032320
var2 = {"key3":3} #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {'key1': 1, 'key2': 2, 'x': 1, 'y': 2}, addr1:2513141032320, var2: {'key3': 3}, addr2:2513141427456
1.2.2.4 集合:两个变量的赋值是共享地址,任一变量修改都会同时生效

  集合的行为与列表相同,两个变量赋值时共享地址,指向同一地址,使用成员变量修改变量,另一个变量也同步生效,但对变量使用集合变量赋值时,变量分裂成完全新的变量

var1 = {1, 2}
var2 = var1  #将变量var1赋值给变量var2,两个变量都指向了同一个地址
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2}, addr1:2276059562720, var2: {1, 2}, addr2:2276059562720
var2.update([3,4]) #变量2赋值后,地址不变,两个变量数据同时生效(同一地址)
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2, 3, 4}, addr1:2276059562720, var2: {1, 2, 3, 4}, addr2:2276059562720
var2 = {6,7} #变量2赋值后,地址变化,两个变量完全分裂成两个对象
print(f'var1: {var1}, addr1:{id(var1)}, var2: {var2}, addr2:{id(var2)}')
#输出:var1: {1, 2, 3, 4}, addr1:2276059562720, var2: {6, 7}, addr2:2276059879616

1.3 拷贝的使用

  简单变量、元组无所谓拷贝,其中,元组成员是不可变的,更谈不上拷贝。列表、字典、集合的拷贝,主要是防止变量给变量赋值后,其中一个变量修改了对象的数据,另外一个变量无法预知数据的修改,导致程序出现混乱,因此,需要做修改隔离。

1.3.1 列表的拷贝

  使用copy进行浅拷贝后,列表变量地址指向不同,但成员却指向同一地址,这是浅拷贝的精髓:节省内存。但修改成员变量后,对应的成员分裂成新的对象(值不同了)。
  从以上行为来看,列表的浅拷贝对原列表变量似乎不存在影响。

var1 = [1,2]
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}, addr1-1: {id(var1[0])}; var2:{var2}, addr2:{id(var2)}, addr2-1:{id(var2[0])}")
#输出:var1:[1, 2],addr1:1615825887296, addr1-1: 140733906224040; var2:[1, 2], addr2:1615826034944, addr2-1:140733906224040
var1.extend([3,4])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[1, 2, 3, 4],addr1:1615825887296; var2:[1, 2], addr2:1615826034944
var1[0] = 9
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[9, 2, 3, 4],addr1:1615825887296; var2:[1, 2], addr2:1615826034944

1.3.2 字典的拷贝

  copy进行浅拷贝后,行为与列表的浅拷贝类似,python会尽量节省内存,对象指向不同地址,但成员指向同一地址,在对成员进行修改时,成员分裂到新的地址,不影响原变量的成员。

var1 = {'x':1, 'y':2}
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}, addr1-1: {id(var1['x'])}; var2:{var2}, addr2:{id(var2)}, addr2-1:{id(var2['x'])}")
#输出:var1:{'x': 1, 'y': 2},addr1:1522714666368, addr1-1: 140733906224040; var2:{'x': 1, 'y': 2}, addr2:1522715059904, addr2-1:140733906224040
var1.update(z=3)
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{'x': 1, 'y': 2, 'z': 3},addr1:1522714666368; var2:{'x': 1, 'y': 2}, addr2:1522715059904
var1['x'] = 5
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{'x': 5, 'y': 2, 'z': 3},addr1:1522714666368; var2:{'x': 1, 'y': 2}, addr2:1522715059904

1.3.3 集合的拷贝

  copy浅拷贝后,对象指向不同的地址,但由于集合无法索引到成员,这里看不到两个变量的成员的地址情况,但这些成员的地址都是共享的。

var1 = {1, 2}
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{1, 2},addr1:1541781897952; var2:{1, 2}, addr2:1541782214848
var1.update({3})
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:{1, 2, 3},addr1:1541781897952; var2:{1, 2}, addr2:1541782214848

注:由于集合的成员都是不可变对象,浅拷贝在集合上不会带来太坏的影响。

1.3.4 浅拷贝带来的坑

  浅拷贝这么好,可以节省内存,那副作用是什么呢? 副作用恐怕无法接受!
  以列表为例,由于列表的成员可以是任意类型,那当然也可以是列表类型!如下的示例中,列表的第1个元素是列表类型,浅拷贝过后,对var1的第1个成员又增加了8,9两个数,从结果可以看到,var1var2的第1个成员数据都被修改了!
  这种坑导致的bug会坑到死~

var1 = [[1],2,3]
var2 = var1.copy()
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1], 2, 3],addr1:3105159749888; var2:[[1], 2, 3], addr2:3105160760000
var1[0].extend([8,9])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1, 8, 9], 2, 3],addr1:3105159749888; var2:[[1, 8, 9], 2, 3], addr2:3105160760000

1.3.5 深拷贝

  既然浅拷贝这么坑,那怎么办呢? python提供了深拷贝,完全克隆一份新的对象,不再考虑节省内存,从些两个变量是路人,互不影响!

import copy
var1 = [[1],2,3]
var2 = copy.deepcopy(var1)
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1], 2, 3],addr1:2852169539648; var2:[[1], 2, 3], addr2:2852171136896
var1[0].extend([8,9])
print(f"var1:{var1},addr1:{id(var1)}; var2:{var2}, addr2:{id(var2)}")
#输出:var1:[[1, 8, 9], 2, 3],addr1:2852169539648; var2:[[1], 2, 3], addr2:2852171136896

注:使用深拷贝需要导入copy模块,然后调用deepcopy()函数。

🕮说明
  从上文中的示例,对于列表、字典对象,浅拷贝时成员还存在地址共享现象,此时修改成员可能会导致浅拷贝出来的对象都受影响,这种非预料的修改,可能会给程序带来灾难性的影响! 因此,必须养成良好的代码习惯,慎用浅拷贝!

作者声明:本文用于记录和分享作者的学习心得,水平有限,难免存在表达错误,欢迎交流和指教!
Copyright © 2022~2024 All rights reserved.


http://www.kler.cn/a/460867.html

相关文章:

  • 打造三甲医院人工智能矩阵新引擎(四):医疗趋势预测大模型篇 EpiForecast与DeepHealthNet合成应用
  • kubernetes学习-kubectl命令、探针(二)
  • MySQL秘籍之索引与查询优化实战指南
  • 设计模式の状态策略责任链模式
  • HTML——57. type和name属性
  • AWS S3文件存储工具类
  • Spring-Mybatis 2.0
  • 【C语言】可移植性陷阱与缺陷(一):应对C语言标准变更
  • 单元测试3.0+ @RunWith(JMockit.class)+mock+Expectations
  • PyTorch快速入门教程【小土堆】之网络模型的保存和读取
  • MAC系统QT Creator的快捷键
  • 运维人员的Python详细学习路线
  • JVM之Class文件详解
  • 【前端】Node.js使用教程
  • 《Vue进阶教程》第三十一课:ref的初步实现
  • 2025元旦源码免费送
  • 探索数据之美,Plotly引领可视化新风尚
  • 代码随想录算法训练营DAY17
  • Rust日志库tklog0.2.9—支持混合时间文件大小备份模式
  • windows下VS release调试
  • Stm32小实验1
  • 【GIS教程】高程点制作DEM并使用ArcgisPro发布高程服务Elevation Layer
  • win32汇编环境下,双击窗口程序内生成的listview列表控件的某行,并提取其内容的示例程序
  • Nmap实用语法简介
  • 使用WebRTC进行视频通信
  • 基于SC-FDE单载波频域均衡MQAM通信链路matlab仿真,包括帧同步,定时同步,载波同步,MMSE信道估计等