牵一发而动全身问题

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)
  1. 深拷贝:使用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网站上进行变量的可视化查看。