在 Python 的学习和实践中,有一个核心概念是绕不开的,那就是"可变"(Mutable)与"不可变"(Immutable)类型。刚开始,你可能觉得这只是个定义问题,但随着你写出更复杂的程序,你会发现,能否深刻理解这两者的区别,直接决定了你的代码是否健壮、高效,以及是否会踩到一些意想不到的"坑"。

这篇文章将带你由浅入深,彻底搞懂这个关键概念。

一、故事从变量赋值说起

在 Python 中,我们常说"变量是贴在对象上的标签"。理解这句话是后续一切的基础。

当你写下 x = 100 时,Python 做了两件事:

  1. 在内存中创建了一个代表数字 100 的对象。
  2. 创建了一个名为 x 的变量(标签),然后把它"贴"到 100 这个对象上。

那么,当我们"修改"变量时,会发生什么呢?这就要看对象的类型了。

二、不可变类型 (Immutable Types)

顾名思义,不可变类型的对象,其值在创建后就不能被改变。任何对它的"修改"操作,实际上都会创建一个全新的对象。

常见的不可变类型包括:

  • 数字: int, float, bool
  • 字符串: str
  • 元组: tuple
  • 冻结集合: frozenset

让我们用代码和内存地址 id() 来眼见为实。

示例 1: 字符串 str

my_string = "hello"
print(f"初始字符串: '{my_string}', 内存地址: {id(my_string)}")

# 尝试"修改"字符串
my_string = my_string + " world"

print(f"修改后字符串: '{my_string}', 内存地址: {id(my_string)}")

输出:

初始字符串: 'hello', 内存地址: 4389754112
修改后字符串: 'hello world', 内存地址: 4389754224

看到了吗?内存地址变了!Python 并没有修改原来的 "hello" 对象,而是创建了一个全新的 "hello world" 对象,然后把 my_string 这个标签从旧对象身上撕下来,贴到了新对象上。

三、可变类型 (Mutable Types)

与不可变类型相反,可变类型的对象,其值可以在创建后被原地修改,而不需要创建新对象。

常见的可变类型包括:

  • 列表: list
  • 字典: dict
  • 集合: set
  • 字节数组: bytearray

示例 2: 列表 list

my_list = [1, 2, 3]
print(f"初始列表: {my_list}, 内存地址: {id(my_list)}")

# 尝试修改列表
my_list.append(4)

print(f"修改后列表: {my_list}, 内存地址: {id(my_list)}")

输出:

初始列表: [1, 2, 3], 内存地址: 4510696320
修改后列表: [1, 2, 3, 4], 内存地址: 4510696320

内存地址完全没变!append 操作是在原始列表对象上直接进行的修改。my_list 这个标签自始至终都贴在同一个对象上。

四、一个常见的陷阱:当元组遇到列表

元组 tuple 是不可变的,对吧?这意味着我们不能增加或删除它的元素。但如果元组里包含了可变类型的对象(比如列表),情况就变得有趣了。

# 元组本身是不可变的
my_tuple = (1, 2, ['a', 'b'])

print(f"初始元组: {my_tuple}, 内存地址: {id(my_tuple)}")

# 尝试修改元组中的列表
my_tuple[2].append('c')

print(f"修改后元组: {my_tuple}, 内存地址: {id(my_tuple)}")

# 尝试直接修改元组元素(这会报错)
# my_tuple[0] = 99 # TypeError: 'tuple' object does not support item assignment

输出:

初始元组: (1, 2, ['a', 'b']), 内存地址: 4474840192
修改后元组: (1, 2, ['a', 'b', 'c']), 内存地址: 4474840192

元组的内存地址没变,但它里面的列表内容却实实在在地改变了。

结论:不可变性指的是对象本身的结构固定。对于元组来说,是它所包含的元素的"引用"不可变。它引用的那个列表还是那个列表(内存地址没变),但列表自身的内容是可以被修改的。

五、为什么这个区别如此重要?

理解可变与不可变,在实际编程中至关重要,尤其体现在以下几个方面:

1. 函数参数的传递

在 Python 中,函数参数传递的是对象的引用。

  • 如果传递的是不可变对象,你在函数内部无法修改原始调用者的变量。
  • 如果传递的是可变对象,你在函数内部的修改会直接影响到原始对象。
def process_data(immutable_str, mutable_list):
    immutable_str = "changed"
    mutable_list.append(99)
    print(f"函数内部: str='{immutable_str}', list={mutable_list}")

s = "original"
l = [1, 2]

process_data(s, l)

print(f"函数外部: str='{s}', list={l}")

输出:

函数内部: str='changed', list=[1, 2, 99]
函数外部: str='original', list=[1, 2, 99]

看到结果了吗?字符串 s 没变,但列表 l 被永久地改变了。

2. 字典的键 (Dictionary Keys)

字典的键必须是不可变类型

这是因为字典的查找效率极高,其内部依赖于对键进行哈希运算(hash())。哈希值要求在对象的生命周期内保持不变。

  • 不可变对象的值固定,哈希值也固定。
  • 可变对象的值可以变,如果允许它当键,它的哈希值也可能变,整个字典的结构就会崩溃。
my_dict = {}
my_dict["key"] = "value"  # 字符串可以当键
my_dict[123] = "value"    # 整数可以当键
my_dict[(1, 2)] = "value" # 元组可以当键

# 尝试用列表当键
try:
    my_dict[[1, 2]] = "value"
except TypeError as e:
    print(e) # 输出: unhashable type: 'list'

3. 函数的默认参数陷阱

这是一个经典的面试题,也是新手最容易犯的错误:永远不要使用可变类型作为函数的默认参数

def add_item(item, item_list=[]):
    item_list.append(item)
    return item_list

# 第一次调用
print(add_item(1)) # 输出: [1]

# 第二次调用
print(add_item(2)) # 输出: [1, 2] (你可能期望的是 [2])

# 第三次调用
print(add_item(3)) # 输出: [1, 2, 3]

原因:函数的默认参数 item_list=[] 只在函数定义时被创建一次。后续所有不提供 item_list 参数的调用,都共享着同一个列表对象。

正确做法:

def add_item_fixed(item, item_list=None):
    if item_list is None:
        item_list = [] # 在函数体内创建新列表
    item_list.append(item)
    return item_list

六、进阶话题与扩展

1. 浅拷贝 vs. 深拷贝:copydeepcopy

Python 标准库 copy 模块提供两种复制策略:

  • 浅拷贝 (copy.copy):仅复制最外层容器,新容器内部仍引用原有子对象。
  • 深拷贝 (copy.deepcopy):递归复制整棵对象图,确保任何层级的修改互不影响。
import copy

a = [1, [2, 3]]
b = copy.copy(a)      # 浅拷贝
c = copy.deepcopy(a)  # 深拷贝

a[1].append(4)
print(b)  # [1, [2, 3, 4]]  —— 受影响
print(c)  # [1, [2, 3]]     —— 不受影响

2. += 运算符:可变与不可变对象的差异

对于不可变对象(如 str, tuple),x += y 会创建 新对象;而对 list 等可变对象,+= 会就地修改。

s = "abc"
print(id(s))

s += "d"
print(id(s))  # 地址变化,说明创建了新对象

lst = [1, 2]
print(id(lst))

lst += [3]
print(id(lst))  # 地址不变,说明原地修改

3. CPython 的对象缓存机制

为了性能,CPython 会缓存 小整数 (-5~256)部分短字符串。因此下面代码在 CPython 中可能打印 True,并不代表语言层面对这些对象做了特殊对待,而是实现细节:

a = 100
b = 100
print(a is b)  # True (CPython)

其它解释器(PyPy、Jython 等)不一定有相同表现,因此不要依赖该特性来做逻辑判断。

4. 并发中的可变对象

在多线程 / 协程场景下,共享可变对象必须采用同步原语,否则会产生竞态条件或数据损坏;不可变对象天然只读,可安全共享。

from threading import Lock

counter = 0
lock = Lock()

def inc():
    global counter
    with lock:
        counter += 1

5. 冻结(只读)数据结构

  • frozenset:不可变集合,可作为字典键;
  • types.MappingProxyType:为字典提供只读视图;
  • 三方库 immutables.Map:高性能、结构共享的持久化不可变映射。

6. 性能小贴士

  • 频繁拼接字符串时,先收集到 list 再使用 ''.join(chunks),可避免创建大量中间对象;
  • 对可变对象使用就地修改操作(如 list.append, 切片赋值)通常更节省内存、提升性能。

总结

特性 不可变类型 (Immutable) 可变类型 (Mutable)
定义 创建后值不能被改变 创建后值可以被原地修改
示例 int, str, tuple, frozenset list, dict, set
修改行为 创建新对象,变量指向新对象 在原对象上修改,变量指向不变
字典键 可以作为字典的键 不可以作为字典的键
函数传参 函数内修改不影响外部原变量 函数内修改会影响外部原变量

掌握 Python 的可变与不可变类型,是写出清晰、可预测且无 Bug 代码的基石。希望这篇文章能让你对这个概念有更深入的理解。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐