今天我们聊一个在 Python 中非常常见,但又容易被忽视的概念——可变对象和不可变对象。作为一个 Python 开发工程师,可能你早就遇到过这个问题,或者在调试过程中因为这个特性踩过坑,今天就带大家全面了解一下这两者的区别以及它们如何影响我们在实际开发中的代码行为。
首先,什么是“可变对象”和“不可变对象”呢?简单来说,可变对象指的是在程序运行过程中,所指向的内存内容是可以改变的,而不可变对象则无法改变其内容。
为啥要区分这两者呢?因为它们在内存管理、赋值操作、函数传参等方面的表现截然不同,理解了这一点,很多我们平时常遇到的问题都会迎刃而解。
不可变对象顾名思义,就是对象的内容一旦创建就无法更改。如果你尝试去修改它们的值,Python 会为你重新分配一个新的内存地址,也就是说,它不会在原地修改内容,而是创建了一个新的对象。这类对象包括基本数据类型如整数(int)、浮点数(float)、字符串(str)和元组(tuple)。
来看看代码示例:
# 不可变对象:int
a = 10
b = a
a += 5
print(a) # 输出:15
print(b) # 输出:10
# 解释:a += 5 其实是a = a + 5的简写操作。Python创建了一个新的整数对象15,而b依然指向原来的对象10。
在上面的例子中,a
最初指向的是整数 10。当我们执行 a += 5
时,Python 实际上是创建了一个新的整数对象 15,并让 a
指向它,而 b
依然指向原来的对象 10。所以,从这里可以看出,整数是不可变对象,不能直接修改其值,而是通过创建新对象的方式来实现“修改”。
同样的道理也适用于字符串和元组:
# 不可变对象:字符串
str1 = "hello"
str2 = str1
str1 = "world"
print(str1) # 输出:world
print(str2) # 输出:hello
这里,str1
和 str2
最初指向同一个字符串对象 "hello"
,但当我们将 str1
赋值为 "world"
时,Python 创建了一个新的字符串对象 "world"
,并将 str1
指向它。str2
依然指向原来的 "hello"
,因此修改 str1
不会影响到 str2
。
与不可变对象相对的是可变对象。可变对象是指对象所指向的内存中的值可以直接修改,不需要重新分配内存空间。
常见的可变对象包括列表(list)、字典(dict)和集合(set)。这类对象的行为和不可变对象不同,当我们修改它们的值时,原始对象会直接发生变化。
来看代码示例:
# 可变对象:列表
list1 = [1, 2, 3]
list2 = list1
list1.append(4)
print(list1) # 输出:[1, 2, 3, 4]
print(list2) # 输出:[1, 2, 3, 4]
在这个例子中,list1
和 list2
都指向同一个列表对象。我们向 list1
中添加了一个元素 4
,这直接改变了原来的列表内容,因为列表是可变对象,所以 list2
也能看到这一变化。不同于不可变对象,这里并没有创建新的对象,而是原地修改了列表内容。
同理,字典和集合也是可变的:
# 可变对象:字典
dict1 = {'a': 1, 'b': 2}
dict2 = dict1
dict1['c'] = 3
print(dict1) # 输出:{'a': 1, 'b': 2, 'c': 3}
print(dict2) # 输出:{'a': 1, 'b': 2, 'c': 3}
这里,当我们给 dict1
添加了一个新键值对时,dict2
也受到了影响,因为它们指向同一个字典对象。
为什么会有这些差异?理解这个差异其实就能解决很多开发中的疑问。不可变对象在修改时会导致新对象的创建,而可变对象在修改时会直接改变原对象。Python 设计中有一个非常重要的概念叫做 内存优化。
不可变对象的不可变特性使得它们可以被安全地共享和复用,Python 在内存管理上会对这些对象进行优化,避免了重复创建相同内容的对象,这就是所谓的“缓存”机制。
比如,整数和字符串是常见的不可变类型,Python 在处理小整数时(通常是 -5 到 256 范围内的整数)会使用一种叫做 对象池 的技术。也就是说,Python 会缓存这些小整数对象,所有变量指向相同值的整数时,它们指向的实际上是同一个内存地址。
# 小整数对象池
a = 256
b = 256
print(id(a) == id(b)) # 输出:True,a和b指向的是同一个对象
# 大整数对象不在池中
c = 257
d = 257
print(id(c) == id(d)) # 输出:False,c和d指向的是不同的对象
这个区别在函数传参时尤为显著。对于可变对象,传递的是对象的引用,而不是对象的副本。因此,函数内修改可变对象,会直接影响到外部的对象。而对于不可变对象,传递的是对象的副本,修改不会影响到原对象。
来看下面的例子:
# 可变对象传参
def modify_list(lst):
lst.append(4)
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # 输出:[1, 2, 3, 4]
在这个例子中,my_list
作为可变对象被传递给函数,函数内对其进行了修改,原始对象 my_list
也发生了变化。
而对于不可变对象,情况不同:
# 不可变对象传参
def modify_integer(x):
x += 5
num = 10
modify_integer(num)
print(num) # 输出:10
这里,虽然 x
在函数内被修改,但 num
在外部没有变化,因为 x
是一个整数(不可变对象),修改 x
时实际上创建了一个新的整数对象。
如果面试官问你:请解释 Python 中的可变对象和不可变对象有什么区别?并举例说明。
你可以这么回答:
在 Python 中,可变对象和不可变对象的区别主要体现在它们的内存管理和赋值行为上。不可变对象是指对象的值一旦创建就不能改变,任何修改操作都会创建一个新的对象。而可变对象的值是可以直接修改的,不需要创建新对象。
例如,整数、浮点数、字符串和元组都是不可变对象。对它们进行修改时,会创建新的对象,而原始对象不受影响。比如:
a = 10
b = a
a += 5 # 会创建新的对象,a指向15,b仍然指向10
而列表、字典和集合是可变对象。对它们进行修改时,原对象会直接发生改变,不会创建新的对象。例如:
list1 = [1, 2, 3]
list2 = list1
list1.append(4) # 修改list1,list2也会看到变化
这种行为会影响到函数传参和内存优化。对于可变对象,修改会直接影响到原对象,而对于不可变对象,修改则不会影响原对象,Python 在内存管理上也对不可变对象进行优化,例如小整数对象池。
明白了这些,可以帮助我们在开发时更好地理解 Python 的行为,避免不必要的错误。
对编程、职场感兴趣的同学,大家可以联系我微信:golang404,拉你进入“程序员交流群”。
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》。