python和numpy中修改变量发生牵一发而动全身的问题

牵一发而动全身问题

python基础

可变数据类型

由于python动态语言的特性,变量定义、赋值等操作有时候可能会出现牵一发动全身的问题,也就是修改一个变量,另一个变量也被修改的情况。例如一个简化的情况

b = [1]
a = [1]
b = a
a[0] = 3
b # -> [3]

此时发现b的值变成了[3],但是如果上面赋给a,b的不是只有一个元素的列表,而是数字,此时发现b的值不会发生改变,这是为什么呢?
要回答这个问题,我们首先要知道如下的概念:

  1. 赋值:在python中,给对象赋值(b=a)实际上是对象的引用。当创建一个对象,然后把它赋值给另一个变量的时候,python只是拷贝了这个对象的引用,这个时候两个变量指向的是同一块内存地址

  2. 浅拷贝:重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。对数据采用浅拷贝的方式时,如果原对象中的元素不可变,那倒无所谓;但如果元素可变,浅拷贝通常会出现一些问题,也就是在不嵌套(对象里面没有子对象)的时候不会出现牵一发而动全身的情况,但是如果有嵌套列表等的时候,列表中的子列表会被动全身。

    1. 数据类型本身的构造器,如list(), set(), dict()等函数
    dict1 = {1:[1,'w'], 2:0, 3:98}
    dict2 = dict(dict1)
    print("dict1 == dict2 ?",dict1 == dict2) # 值相等
    print("dict1 is dict2 ?",dict1 is dict2) # 但是它们指向的内存空间不同
    print(id(dict1),id(dict2)) # 而且id也不同
    
    1. 可变序列类型(例如列表、np数组)的切片:操作符
    list1 = [[1], [2], [3]]
    list2 = list1[:]
    list3 = list1[0]
    list1[0][0] = [520]
    list2 # -> [[520], [2], [3]]
    list3 # -> [520]
    # 但是改成list1 = [1,2,3],那么都不会发生牵一发而动全身的情况
    

    对于字典和集合而言,不能使用切片操作符:来完成浅拷贝

    1. copy.copy()函数两种方式来完成浅拷贝
    import copy
    list1 = [1, 2, 3]
    list2 = copy.copy(list1)
    
  3. 深拷贝:使用copy.deepcopy()来实现对象的深拷贝

不可变数据类型

什么是可变和不可变数据类型呢? 首先我们需要知道,在定义变量的时候,是先创建一块内存空间,将值放进去,然后变量名里存放着该内存空间的内存地址。

  1. 可变类型:在不改变内存空间的情况下,可以改变其值。也即在id()保持固定的情况下,值可以被改变
  2. 不可变类型:具有固定值的对象,包括数字、字符串和元组。如果必须存储一个不同的值,则必须创建新的对象。
    在python中,每个对象都有各自的编号类型。一个对象被创建后,它的编号(id)和类型绝不会改变。is运算符比较两个对象的编号是否相同;id()函数返回一个代表其编号的整型数。type()函数返回一个对象的类型(类型本身也是对象)。具体的知识可以见这里
    对于元组、字符串等不可变数据类型,上述浅拷贝方法会开辟新的内存地址,但其中存放的是指向相同元组的引用(有点像指针)
import copy
set1 = 'operation'
set2 = copy.copy(set1)
print(set2)
print("set1 == set2 ?",set1 == set2)
print("set1 is set2 ?",set1 is set2)

Numpy基础

在numpy中,直接赋值b=a则两个变量的内存地址是一样的,这点和python基础中的一样。而切片也会造成牵一发而动全身的情况。
其他的还有view()方法,
numpy中的拷贝有copy()方法和数组索引,切片和数组组合索引相结合返回的也是拷贝。
拷贝还有reshape()方法等
(注意,在numpy中没有浅拷贝和深拷贝之分,统一称为拷贝)

import numpy as np
a = np.array([[1,3,5,7], [4,6,8,10]])
b = a
c = a.copy()
print('a的地址:', id(a))
print('b的地址:', id(b))
print('c的地址:', id(c))
b[0, 0] = 520
print('a is\n',a) # 改变b使得a也被改变
c[1, 3] = 521
print('c is\n',c) # c被改变了
print('a is\n',a) # 但是a没有被改变
a = np.array([1,2,3,4,5])
qiepian = a[2:]
qiepian[0] = 5200
print('qiepian is\n', qiepian)
print('a is\n',a) # 切片也会牵一发而动全身

还有两种情况是:

  1. Numpy数组作为参数对象输入函数,在函数中修改数组的值,则原来数组也会被修改
  2. Numpy数组的形状改变时候,会重新分配新的内存存储改变形状的新数组,但是view()方法不会。

一个可视化神器

为了方便、直观地看出来到底是引用还是浅拷贝还是深拷贝,我们可以在pythontutor网站上进行变量的可视化查看。

参考文章

https://blog.csdn.net/qq_40630902/article/details/119278072
https://www.cnblogs.com/liqianxin/p/12426525.html
https://zhuanlan.zhihu.com/p/371403187?utm_source=wechat_session&utm_medium=social&utm_oi=549681733273067520&utm_campaign=shareopn

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值