深入理解Python中的可变与不可变对象

34次阅读
没有评论

共计 1988 个字符,预计需要花费 5 分钟才能阅读完成。

在 Python 编程中,“一切皆对象”是一个核心理念,而“可变对象”和“不可变对象”的区别则是许多初学者和中级开发者在调试、函数传参、数据结构选择等过程中经常遇到的迷惑点。掌握这两个概念,对于写出高质量、少 Bug 的 Python 代码至关重要。

一、Python 中“对象”的基本理解
Python 中的每一个值都是一个对象,每个对象都有:

  1. 身份(Identity):可以用 id(obj) 来查看,标识对象在内存中的地址;
  2. 类型(Type):用 type(obj) 查看;
  3. 值(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

八、可变与不可变对象的选择建议

  1. 若你希望数据结构是只读的,优先使用元组或 namedtuple;
  2. 如果逻辑上需要频繁修改,使用 list 或 dict;
  3. 在函数中修改数据,请注意是否会对调用者的对象产生副作用;
  4. 避免使用可变对象作为默认参数。

九、练习题与思考

  1. 请写一个函数,接受一个字符串列表,返回所有长度大于 5 的元素组成的新列表,但不修改原列表;
  2. 请写一个函数,传入一个字典,返回去除所有值为 None 的新字典(要求不影响原始字典);
  3. 编写代码证明两个变量是否指向同一个对象。

十、总结
可变对象与不可变对象是 Python 中一个底层且重要的概念,它不仅决定了变量赋值的行为,也影响函数调用、数据结构设计、性能优化等多个方面。理解它们之间的区别,有助于我们编写出更加稳定、清晰和高效的 Python 代码。

正文完
 0
评论(没有评论)