Python 中的可变和不可变对象

Python 中可变与不可变数据类型的解读。

在介绍 Python 基本数据结构时我们说过:

数值类型(Number)、字符串类型(String)以及元组(Tuple)是不可变类型,而列表(List)、集合(Set)和字典(Dict)是可变数据类型。

那,什么是可变和不可变类型?


认识可变对象和不可变对象

不可变对象: >>>> 是指 该对象所指向的内存中的元素值是不能被改变的。当需要改变某个变量时候,由于其所指的值不能被改变,只好把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。

可变对象: >>>> 是指 该对象所指向的内存中的元素值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变

不理解?往下看:


is && == && id()

在开始正式的解读之前,我们先来看几个必要的语法:

[1] >>>> id(object)

Python 中的 id(object) 函数可以用于获取元素对象的内存地址。语法规则如下:

1
id(object)

样例如下:

1
2
3
4
5
6
>>> str1 = "id(object) function test"
>>> num = 1.23
>>> id(str1)
2419096572024
>>> id(num)
2419065860360

[2] >>>> is && ==

Python 中经常会用到对象之间的比较,可以用 ==,也可以用 is 。它们有什么区别?

  1. is:比较的是 两个实例对象是不是完全相同是不是同一个对象(占用的内存地址是否相同,内容相同)
  2. ==:比较的是 两个对象的内容是否相等,即内存地址可以不一样,内容一样就可以了

样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> str1 = "Python"
>>> str2 = str1

>>> str1 is str2
True

>>> id(str1)
2419095062488
>>> id(str2)
2419095062488

>>> str1 == str2
True

不可变对象样例

接下来,我们使用 is 判断两个对象的 id(内存地址)是否相同, 而使用 == 判断的则是内容是否相同。

[1] >>>> 数值类型(Number):

1
2
3
4
5
6
7
8
9
10
>>> num_a = 2
>>> id(num_a)
140724509286176
>>> id(2)
140724509286176
>>> num_a = 2 + 2
>>> id(num_a)
140724509286240
>>> id(2)
140724509286176

不考虑常量缓冲池啥的,我们可以这么理解:变量数值(Number 类型)从 2 -> 4 ,不是使用原来数值 2 的内存空间(数值类型不可变),而是会在内存中新开辟一个空间。

[2] >>>> 字符串(String):

1
2
3
4
5
6
>>> str_test = "good"
>>> id(str_test)
1365047640168
>>> str_test = "good" + "boy"
>>> id(str_test)
1365047641792

可以发现,同样变量数值(字符串)变化后,其所对应的内存地址也会变化。

由于变量是不可变对象的引用,变量对应内存的值是不允许被改变。

故,当变量要改变时,相对于是创建了一个新对象,开辟一个新的地址,然后将变量(引用)再指向这个新的地址(所以前后 str_testid 不一样)。

[3] >>>> 元组(Tuple):

1
2
3
4
5
>>> tup1 = (1, 2, 3)
>>> tup1[1] = 6
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

可以发现,当我们想要更改元组对象 (1, 2, 3) 中元素值时被阻止。这就是因为元组为不可变对象,强行修改其元素值会报错。

有些人可能发现下面的例子,元组不是变化了么:

1
2
3
4
>>> tup1 = ("test", 1.023, [1, 2])
>>> tup1[2][0] = 888
>>> tup1
('test', 1.023, [888, 2])

直观上看,确实发现元组的值变了。但其实变的不是 tuple 的元素,而是 list 的元素。tuple 一开始指向的 list 并没有改成别的 list。所以,tuple 所谓的“不变”是说,tuple 的每个元素,指向永远不变。所以又如下:

1
2
3
4
5
6
>>> tup1 = ("test", 1.023, [1, 2])
>>> id(tup1[2])
1365045699272
>>> tup1[2][0] = 888
>>> id(tup1[2])
1365045699272

确实,tup1[2] 所对应的元素对象确实还是原来的,并没有变化。


可变对象样例

[1] >>>> 列表(List):

1
2
3
4
5
6
7
8
>>> list1 = [1, 2, 3]
>>> id(list1)
1365046919496
>>> list1[1] = 888
>>> list1
[1, 888, 3]
>>> id(list1)
1365046919496

可以发现,此时我们修改列表实例中元素值时已经被允许。并且,修改变量之后,其地址并未发生变法,也就是没有开辟新空间。

List 赋值的情况 >>>>

1
2
3
4
5
>>> list2 = list1
>>> print(id(list1), id(list2))
1365046919496 1365046919496
>>> print(list1 is list2)
True

list1 赋值给 list2,事实上,list1 是对对象的引用,list2 = list1 即引用的传递,现在两个引用都指向了同一个对象(地址)。所以其中一个变化,会影响到另外一个:

1
2
3
>>> list1[2] = 999
>>> list2
[1, 888, 999]

而相对于元组不可变类型进行赋值,你会发现如下:

1
2
3
4
5
>>> tup1 = (1, 2, 3)
>>> tup2 = tup1
>>> tup1 = (2, 3, 888)
>>> tup2
(1, 2, 3)

[2] >>>> 集合(Set):

1
2
3
4
5
6
7
>>> set1 = {1, 2, 3}
>>> set2 = set1
>>> print(id(set1), id(set2))
1365047636712 1365047636712
>>> set1.add(4)
>>> set2
{1, 2, 3, 4}

可以发现,集合和上面列表(List)的情况是相同的,均为可变对象。

可变对象由于所指对象可以被修改,所以无需复制一份之后再改变,直接原地改变,所以不会开辟新的内存,改变前后 id 不变。对于赋值,传递的是引用,地址内部发生变化的话,两个变量值都会变化。

而对于不可变对象就不是这样了, 可以和这个对比一下:

1
2
3
4
5
>>> test1 = 123
>>> test2 = test1
>>> test1 = 456
>>> test2
123

[3] >>>> 深、浅拷贝:

如果只是简单的想拷贝,仅仅就是将内容拷贝过去,传递给新变量的是内容而不是引用。这在想使用列表的值又不想修改原列表的时候特别有用。可以看作浅拷贝:

1
2
3
4
>>> list2 = [1,2,3]
>>> list3 = list2[:]
>>> print(id(list2), id(list3))
1365046189192 1365047660616

那么,相反的,上面 List 的赋值可以看作是深拷贝,传递的是引用。

关于 Python 中的深、浅拷贝可参见: Python 中的深浅拷贝详解


可变不可变对参数传递的影响

基于上述赋值思考,我们应该可以知道:作为函数参数,也应该是一样的,可变类型传递的是引用,不可变类型传递的是内容。

1
2
3
4
5
6
7
8
9
10
11
12
>>> test_list = [1, 2, 3, 4]
>>> test_str = 'HAHA'
>>> def change(alist):
... alist.append(5)
...
>>> def not_change(astr):
... astr.lower()
...
>>> change(test_list)
>>> not_change(test_str)
>>> print(test_list, test_str)
[1, 2, 3, 4, 5] HAHA

当然了,如果不想改变原来列表的值,参数可以传入列表的拷贝:alsit[:]

什么传递的是引用,传递的是内容?这太迷惑了!还是回归上面的可变、不可变对象理解:你可以认为函数接收到参数后,具体对参数的操作是在函数内完成的,就是参数元素对象已经进入函数,具体操作是否会影响传入前参数元素对象,这要遵循上面可变和不可变对象的说明。


Be Careful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> a1 = [1, 2, 3]
>>> a2 = a1
# 此时我们知道,内存地址是相同的:
>>> print(id(a1), id(a2))
1365047156616 1365047156616

# 此时,等式中,右边的 a2 还是和 a1 的 id 一样的。但一旦赋值成功,a2 就指向新的对象:
>>> a2 = a2 + [4]
>>> print(id(1), id(a2))
140723501621904 1365047660808
>>> a3 = a1 + []
>>> print(id(1), id(a3))
140723501621904 1365047660936
>>> print(a1, a2, a3)
[1, 2, 3] [1, 2, 3, 4] [1, 2, 3]

+ 运算符可以将多个(同类型)序列连接起来,对于列表而言,相当于在第一个列表的末尾添加了另一个列表,生成一个新的列表返回。

如果这样写:

1
2
3
4
5
6
7
>>> a1 = [1, 2, 3]
>>> a2 = a1
>>> print(id(a1), id(a2))
1365047156680 1365047156680
>>> a2 += [4]
>>> print(id(a1), id(a2))
1365047156680 1365047156680

不同的地方在于 a2 += [4],这句相当于调用了 a2.extend([4]) 相当于原地改变,并没有新的对象产生。


Author

Waldeinsamkeit

Posted on

2018-01-08

Updated on

2022-03-18

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.