共计 1988 个字符,预计需要花费 5 分钟才能阅读完成。
在 Python 编程中,“一切皆对象”是一个核心理念,而“可变对象”和“不可变对象”的区别则是许多初学者和中级开发者在调试、函数传参、数据结构选择等过程中经常遇到的迷惑点。掌握这两个概念,对于写出高质量、少 Bug 的 Python 代码至关重要。
一、Python 中“对象”的基本理解
Python 中的每一个值都是一个对象,每个对象都有:
- 身份(Identity):可以用
id(obj)来查看,标识对象在内存中的地址; - 类型(Type):用
type(obj)查看; - 值(Value):即对象所代表的数据内容。
Python 使用引用模型(引用计数机制)进行变量绑定,这意味着当你写下 a = [1, 2, 3],实际上是将变量名 a 绑定到了内存中的一个列表对象上。
二、可变对象与不可变对象的区别
| 类型 | 可变性 | 常见类型 |
|---|---|---|
| 不可变对象 | 不可更改 | int, float, str, tuple |
| 可变对象 | 可更改 | list, dict, set, bytearray |
不可变对象(Immutable):其值一旦创建,就无法修改。任何看似的修改,实质上是创建了新对象。
a = 'hello'
print(id(a)) # 假设为 140123456
a += ' world'
print(id(a)) # 身份变了,表示创建了新字符串对象
可变对象(Mutable):其值可以原地更改,内存地址不变。
b = [1, 2, 3]
print(id(b)) # 假设为 140987654
b.append(4)
print(id(b)) # 身份不变,表示对象被修改
三、函数参数传递中的可变性陷阱
Python 所有的参数传递都是“对象引用的传递”,但是否会影响原值取决于对象是否可变。
示例:不可变类型不会被改变
def add_one(x):
x += 1
return x
a = 5
add_one(a)
print(a) # 输出仍然是 5
示例:可变类型会被原地修改
def append_item(lst):
lst.append(100)
nums = [1, 2, 3]
append_item(nums)
print(nums) # 输出: [1, 2, 3, 100]
这就是为什么我们有时需要对可变对象进行“显式复制”来避免副作用。
四、拷贝(copy)与赋值(assignment)的区别
赋值只是创建一个新的引用:
a = [1, 2]
b = a
b.append(3)
print(a) # 输出: [1, 2, 3]
而拷贝则是创建一个新的对象:
import copy
a = [1, 2]
b = copy.copy(a)
b.append(3)
print(a) # 输出: [1, 2]
print(b) # 输出: [1, 2, 3]
注意:copy.copy() 是浅拷贝,若列表中有嵌套对象则仍然共享引用;而 copy.deepcopy() 会递归地复制所有内容。
五、元组是否一定不可变?
很多人以为 tuple 是绝对不可变的,但若元组中包含了可变对象,如列表,它们的内容仍然可以改变:
t = ([1, 2], 3)
t[0].append(4)
print(t) # 输出: ([1, 2, 4], 3)
这说明元组本身的结构(索引关系)是不可变的,但内部的元素若是可变对象,它们的值仍然可以被更改。
六、字符串拼接与性能:为何推荐 join
字符串属于不可变类型,所以使用 + 拼接多个字符串时,会产生多个中间对象,影响性能。推荐使用 str.join():
# 不推荐
result = ''
for s in ['a', 'b', 'c']:
result += s
# 推荐
result = ''.join(['a', 'b', 'c'])
七、函数默认参数的可变性陷阱
定义函数时若将可变对象作为默认参数值,会导致“共享默认值”的问题:
def add_to_list(val, lst=[]):
lst.append(val)
return lst
print(add_to_list(1)) # [1]
print(add_to_list(2)) # [1, 2],不是你期望的行为
正确做法:
def add_to_list(val, lst=None):
if lst is None:
lst = []
lst.append(val)
return lst
八、可变与不可变对象的选择建议
- 若你希望数据结构是只读的,优先使用元组或 namedtuple;
- 如果逻辑上需要频繁修改,使用 list 或 dict;
- 在函数中修改数据,请注意是否会对调用者的对象产生副作用;
- 避免使用可变对象作为默认参数。
九、练习题与思考
- 请写一个函数,接受一个字符串列表,返回所有长度大于 5 的元素组成的新列表,但不修改原列表;
- 请写一个函数,传入一个字典,返回去除所有值为 None 的新字典(要求不影响原始字典);
- 编写代码证明两个变量是否指向同一个对象。
十、总结
可变对象与不可变对象是 Python 中一个底层且重要的概念,它不仅决定了变量赋值的行为,也影响函数调用、数据结构设计、性能优化等多个方面。理解它们之间的区别,有助于我们编写出更加稳定、清晰和高效的 Python 代码。