Python 中的可变和不可变对象
Python 中可变与不可变数据类型的解读。
在介绍 Python 基本数据结构时我们说过:
数值类型(Number)、字符串类型(String)以及元组(Tuple)是不可变类型,而列表(List)、集合(Set)和字典(Dict)是可变数据类型。
那,什么是可变和不可变类型?
认识可变对象和不可变对象
不可变对象: >>>> 是指 该对象所指向的内存中的元素值是不能被改变的。当需要改变某个变量时候,由于其所指的值不能被改变,只好把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
可变对象: >>>> 是指 该对象所指向的内存中的元素值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
不理解?往下看:
is && == && id()
在开始正式的解读之前,我们先来看几个必要的语法:
[1] >>>> id(object)
Python 中的 id(object) 函数可以用于获取元素对象的内存地址。语法规则如下:
1 | id(object) |
样例如下:
1 | "id(object) function test" str1 = |
[2] >>>> is && ==
Python 中经常会用到对象之间的比较,可以用 ==
,也可以用 is
。它们有什么区别?
- is:比较的是 两个实例对象是不是完全相同,是不是同一个对象(占用的内存地址是否相同,内容相同);
- ==:比较的是 两个对象的内容是否相等,即内存地址可以不一样,内容一样就可以了。
样例如下:
1 | "Python" str1 = |
不可变对象样例
接下来,我们使用 is
判断两个对象的 id(内存地址)是否相同, 而使用 ==
判断的则是内容是否相同。
[1] >>>> 数值类型(Number):
1 | 2 num_a = |
不考虑常量缓冲池啥的,我们可以这么理解:变量数值(Number 类型)从 2 -> 4
,不是使用原来数值 2
的内存空间(数值类型不可变),而是会在内存中新开辟一个空间。
[2] >>>> 字符串(String):
1 | "good" str_test = |
可以发现,同样变量数值(字符串)变化后,其所对应的内存地址也会变化。
由于变量是不可变对象的引用,变量对应内存的值是不允许被改变。
故,当变量要改变时,相对于是创建了一个新对象,开辟一个新的地址,然后将变量(引用)再指向这个新的地址(所以前后 str_test
的 id
不一样)。
[3] >>>> 元组(Tuple):
1 | 1, 2, 3) tup1 = ( |
可以发现,当我们想要更改元组对象 (1, 2, 3)
中元素值时被阻止。这就是因为元组为不可变对象,强行修改其元素值会报错。
有些人可能发现下面的例子,元组不是变化了么:
1 | "test", 1.023, [1, 2]) tup1 = ( |
直观上看,确实发现元组的值变了。但其实变的不是 tuple
的元素,而是 list
的元素。tuple 一开始指向的 list 并没有改成别的 list。所以,tuple 所谓的“不变”是说,tuple 的每个元素,指向永远不变。所以又如下:
1 | "test", 1.023, [1, 2]) tup1 = ( |
确实,tup1[2] 所对应的元素对象确实还是原来的,并没有变化。
可变对象样例
[1] >>>> 列表(List):
1 | 1, 2, 3] list1 = [ |
可以发现,此时我们修改列表实例中元素值时已经被允许。并且,修改变量之后,其地址并未发生变法,也就是没有开辟新空间。
List 赋值的情况 >>>>
1 | list2 = list1 |
将 list1
赋值给 list2
,事实上,list1
是对对象的引用,list2 = list1
即引用的传递,现在两个引用都指向了同一个对象(地址)。所以其中一个变化,会影响到另外一个:
1 | 2] = 999 list1[ |
而相对于元组不可变类型进行赋值,你会发现如下:
1 | 1, 2, 3) tup1 = ( |
[2] >>>> 集合(Set):
1 | 1, 2, 3} set1 = { |
可以发现,集合和上面列表(List)的情况是相同的,均为可变对象。
可变对象由于所指对象可以被修改,所以无需复制一份之后再改变,直接原地改变,所以不会开辟新的内存,改变前后 id 不变。对于赋值,传递的是引用,地址内部发生变化的话,两个变量值都会变化。
而对于不可变对象就不是这样了, 可以和这个对比一下:
1 | 123 test1 = |
[3] >>>> 深、浅拷贝:
如果只是简单的想拷贝,仅仅就是将内容拷贝过去,传递给新变量的是内容而不是引用。这在想使用列表的值又不想修改原列表的时候特别有用。可以看作浅拷贝:
1 | 1,2,3] list2 = [ |
那么,相反的,上面 List 的赋值可以看作是深拷贝,传递的是引用。
关于 Python 中的深、浅拷贝可参见: Python 中的深浅拷贝详解
可变不可变对参数传递的影响
基于上述赋值思考,我们应该可以知道:作为函数参数,也应该是一样的,可变类型传递的是引用,不可变类型传递的是内容。
1 | 1, 2, 3, 4] test_list = [ |
当然了,如果不想改变原来列表的值,参数可以传入列表的拷贝:alsit[:]
。
什么传递的是引用,传递的是内容?这太迷惑了!还是回归上面的可变、不可变对象理解:你可以认为函数接收到参数后,具体对参数的操作是在函数内完成的,就是参数元素对象已经进入函数,具体操作是否会影响传入前参数元素对象,这要遵循上面可变和不可变对象的说明。
Be Careful
1 | 1, 2, 3] a1 = [ |
+
运算符可以将多个(同类型)序列连接起来,对于列表而言,相当于在第一个列表的末尾添加了另一个列表,生成一个新的列表返回。
如果这样写:
1 | 1, 2, 3] a1 = [ |
不同的地方在于 a2 += [4]
,这句相当于调用了 a2.extend([4])
相当于原地改变,并没有新的对象产生。
install_url
to use ShareThis. Please set it in _config.yml
.