python与C/C++不一样,它的变量使用有自己的特点,当初学python的时候,一定要记住“一切皆为对象,一切皆为对象的引用”这句话,其实这个特点类似于JAVA,所以在python里面大家也不用担心类似于C/C++中的指针的复杂问题。下面本文将对python里面的“可变数据类型”和“不可变数据类型”进行分析。
首先,我们需要知道在python中哪些是可变数据类型,哪些是不可变数据类型。可变数据类型:列表list和字典dict,set,自己定义的类对象,numpy中的ndarray对象,具体参考:NumPy:拷贝和视图。不可变数据类型:整型int、浮点型float、字符串型string和元组tuple,以及frozenset。(注意:字典的key只能是不可变对象,即字典的key只能是整型int、浮点型float、字符串型string和元组tuple)。
set(): 一种可变的、无序的、有限的集合,其元素是唯一的、不可变的(可哈希的)对象。
frozenset(): 一种不可变的、可哈希的、无序的集合,其元素是唯一的,不可变的哈希对象。
其次需要注意,python中的变量保存的都是值所在的地址,例如 a = 10,a保存的是10所在的地址。这里的可变与不可变指的是指向可变或者不可变。
然后,我们以int和list为例,来看看“可变数据类型”和“不可变数据类型”到底有什么区别。
(1) 不可变数据类型分析 。先来看一段程序:
>>> x = 1
>>> id(x)
31106520
>>> y = 1
>>> id(y)
31106520
>>> x == y
True
>>> x is y #具有相同值的不可变对象(例如整数)是同一对象
True
>>> x = 2
>>> id(x)
31106508
>>> y = 2
>>> id(y)
31106508
>>> z = x
>>> id(x)
31106508
>>> x += 2
>>> id(x) # 只要不可变对象指向的值发生变化,不可变对象就会重新指向新的值(即4)所在的地址
31106484
>>>x
4
>>>z #x都已经指向了新的值所在地址,所以肯定不会影响z的值
2
>>>a = "Hello_World"
>>>b = "Hello_World"
>>>a is b
True
>>>a = "Hello World" # 字符串中出现了非标识符时,不采用驻留,所以a与b不指向同一内存地址
>>>b = "Hello World"
>>>a is b
False
>>>a = -5
>>>b = -5
>>>a is b
True
>>>a = -6
>>>b = -6
>>>a is b
False
>>>a = 256
>>>b = 256
>>>a is b
True
>>>a = 257
>>>b = 257 # 整型的界限是(-5,256),只有这个范围内才驻留
>>>a is b
False
>>> str1='sten'+'waves'
>>> str2 is 'stenwaves'
True
>>> str3='sten'
>>> str4=str3+'waves'
>>> str4 is 'stenwaves'
False
上面这段程序都是对不可变数据类型中的int类型的操作,id()查看的是当前变量的地址值。我们先来看x = 1和y = 1两个操作的结果,从上面的输出可以看到x和y在此时的地址值是一样的,也就是说x和y其实是引用了同一个对象,即1,也就是说内存中对于1只占用了一个地址,而不管有多少个引用指向了它,都只有一个地址值,只是有一个引用计数会记录指向这个地址的引用到底有几个而已。当我们进行x = 2赋值时,发现x的地址值变了,虽然还是x这个引用,但是其地址值却变化了,后面的y = 2以及z = y,使得x、y和z都引用了同一个对象,即2,所以地址值都是一样的。当x和y都被赋值2后,1这个对象已经没有引用指向它了,所以1这个对象占用的内存,即31106520地址要被“垃圾回收”,即1这个对象在内存中已经不存在了。最后,x进行了加2的操作,所以创建了新的对象4,x引用了这个新的对象,而不再引用2这个对象。
那么为什么称之为不可变数据类型呢?这里的不可变大家可以理解为x引用的地址处的值是不能被改变的,也就是31106520地址处的值在没被垃圾回收之前一直都是1,不能改变,如果要把x赋值为2,那么只能将x引用的地址从31106520变为31106508,相当于x = 2这个赋值又创建了一个对象,即2这个对象,然后x、y、z都引用了这个对象,所以int这个数据类型是不可变的,如果想对int类型的变量再次赋值,在内存中相当于又创建了一个新的对象,而不再是之前的对象。从下图中就可以看到上面程序的过程。
从上面的过程可以看出,不可变数据类型的优点就是内存中不管有多少个引用,相同的对象只占用了一块内存,但是它的缺点就是当需要对变量进行运算从而改变变量引用的对象的值时,由于是不可变的数据类型,所以必须创建新的对象,这样就会使得一次次的改变创建了一个个新的对象,不过不再使用的内存会被垃圾回收器回收。
(2)可变数据类型分析。下面同样先看一段程序。
>>> a = [1, 2, 3]
>>> id(a)
1468022383176
>>> b = [1, 2, 3]
>>> id(b)
1468022383048
>>> a == b
True
>>> a is b #具有相同值的可变对象(例如list)是不同的对象
False
>>> c = a
>>> a.append(4)
>>> id(a)
1468022383176
>>> a += [2] #与不可变对象不同,可变对象并不是只要改变其值就会指向新地址,只有给可变对象赋新的值或者某些场景下可变对象才会指向新的地址。比如 +=就不会让可变对象指向新的地址。而 = +却会指向新地址。
>>> id(a)
1468022383176
>>> a
[1, 2, 3, 4, 2]
>>> id(c)
1468022383176
>>> c # c会与a同步变化
[1,2,3,4,2]
>>> a = a +[9] #a会指向新地址
>>>id(a)
1467885024648
>>>a
[1,2,3,4,2,9]
>>>c #a与c已经不指向同一地址,所以a的变化肯定不会影响c了
[1,2,3,4,2]
>>>id(c)
1468022383176
从上面的程序中可以看出,进行a = [1, 2, 3] 和 b=[1,2,3] 操作,两次值相同的对象引用的地址是不同的,也就是说其实创建了两个不同的对象,这一点明显不同于不可变数据类型,所以对于可变数据类型来说,具有同样值的对象是不同的对象,即在内存中保存了多个同样值的对象,地址值不同。接着来看后面的操作,我们对列表进行添加操作,分别a.append(4)和a += [2],发现这两个操作使得a引用的对象值变成了上面的最终结果,但是a引用的地址依旧是1468022383176,也就是说对a进行的操作不会改变a引用的地址值,只是在地址后面又扩充了新的地址,改变了地址里面存放的值(注意:a += [2] 与 a = a + [2]是不同的,后者会指向新地址,*=同理。),所以可变数据类型的意思就是说对一个变量进行操作时,其值是可变的,值的变化并不会引起新建对象,即地址是不会变的,只是地址中的内容变化了或者地址得到了扩充。下图对这一过程进行了图示,可以很清晰地看到这一过程。
从上述过程可以看到,可变数据类型是允许同一对象的内容,即值可以变化,但是地址是不会变化的。但是需要注意一点,对可变数据类型的操作不能是直接进行新的赋值操作,比如说a = [1, 2, 3, 4, 5, 6, 7],这样的操作就不是改变值了,而是新建了一个新的对象,这里的可变只是对于类似于append、+=等这种操作。
总之,用一句话来概括上述过程就是:“python中的不可变数据类型,不允许变量的值发生变化,如果改变了变量的值,相当于是新建了一个对象,而对于相同的值的对象,在内存中则只有一个对象,内部会有一个引用计数来记录有多少个变量引用这个对象;可变数据类型,允许变量的值发生变化,即如果对变量进行append、+=等这种操作后,只是改变了变量的值,而不会新建一个对象,变量引用的对象的地址也不会变化,不过对于相同的值的不同对象,在内存中则会存在不同的对象,即每个对象都有自己的地址,相当于内存中对于同值的对象保存了多份,这里不存在引用计数,是实实在在的对象。”
由于python规定参数传递都是传递引用,也就是传递给函数的是原变量实际所指向的内存空间(就把python中函数的参数传递理解成赋值号(=),即让形参指向实参指向的位置),修改的时候就会根据该引用的指向去修改该内存中的内容,所以按道理说我们在函数内改变了传递过来的参数的值的话,原来外部的变量也应该受到影响。但是上面我们说到了python中有可变类型和不可变类型,这样的话,当传过来的是可变类型(list,dict)时,我们在函数内部修改就会影响函数外部的变量。而传入的是不可变类型时在函数内部修改改变量并不会影响函数外部的变量,因为修改的时候会先复制一份再修改。下面通过代码证明一下:
def test(a_int, b_list):
a_int = a_int + 1
b_list.append('13')
print('inner a_int:' + str(a_int))
print('inner b_list:' + str(b_list))
if __name__ == '__main__':
a_int = 5
b_list = [10, 11]
test(a_int, b_list)
print('outer a_int:' + str(a_int))
print('outer b_list:' + str(b_list))
运行结果如下:
inner a_int:6
inner b_list:[10, 11, '13']
outer a_int:5
outer b_list:[10, 11, '13']
所以,当函数形参是可变对象(list,dict等)时,要想在函数内部修改改变量时不影响函数外部的变量,可以给函数形参传递可变对象的复制,例如a = [1,2,3]作为形参是a[:]
这篇文章看完之后,可以看下面这篇,很有相关性。