Python 可变与不可变对象:从列表、元组到自定义类

2026-09-16 25 预计阅读时间: 1 分钟
来源: realpython.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

在 Python 中,“可变”描述的不是变量能否重新赋值,而是对象创建后,其内部状态能否发生变化。这个区别会直接影响函数参数、容器操作、字典键、对象共享以及自定义类的设计。理解对象身份、值和变量绑定之间的关系,比单纯背诵类型列表更重要。

变量保存的是对象引用

列表、字典和集合通常是可变对象;整数、字符串、元组通常被视为不可变对象。不过,变量本身没有“可变”或“不可变”的属性,它只是绑定到某个对象。

下面的程序可以直接运行:

numbers = [1, 2]
alias = numbers

print(numbers is alias)  # True
numbers.append(3)
print(alias)             # [1, 2, 3]

name = "Python"
old_name = name
name += " 3"

print(name)              # Python 3
print(old_name)          # Python
print(name is old_name)  # False

append() 修改了原列表,因此 numbersalias 都能看到变化。字符串不能原地修改,name += " 3" 实际上创建了新字符串,再让 name 指向它;old_name 仍然指向原对象。

判断行为时,可以问两个问题:

  1. 这次操作是在修改原对象,还是创建新对象?
  2. 是否还有其他变量指向同一个对象?

id()is 可以帮助观察对象身份,但业务代码通常应使用 == 比较值。除 None 等单例外,不要用 is 代替值比较。

元组不可变,不代表里面的一切都不可变

元组不能替换、增加或删除元素,但它可以引用可变对象。因此,“元组不可变”准确地说,是元组保存的引用序列不能改变。

record = ("alice", ["python"])
record[1].append("sql")

print(record)  # ('alice', ['python', 'sql'])

try:
    record[0] = "bob"
except TypeError as exc:
    print(type(exc).__name__, exc)

列表内容可以变化,但 record[0] 不能被重新赋值。这一点也会影响元组能否作为字典键:只有其所有成员都可哈希时,元组才可哈希。

valid = {(2025, 3): "release"}
print(valid[(2025, 3)])

try:
    invalid = {("alice", ["python"]): "profile"}
except TypeError as exc:
    print(type(exc).__name__, exc)

字典依靠键的哈希值定位数据。如果一个键在存入后发生变化,它的哈希和相等性语义可能失效。因此,列表和字典不能直接作为字典键,而只包含可哈希成员的元组通常可以。

函数参数中的共享修改

Python 调用函数时,会把对象引用绑定到形参。函数能否影响调用方看到的内容,取决于它是修改对象,还是只重新绑定局部变量。

def add_item(items, item):
    items.append(item)


def add_item_copy(items, item):
    return [*items, item]


def reset(items):
    items = []
    return items

original = ["a"]
add_item(original, "b")
print(original)  # ['a', 'b']

updated = add_item_copy(original, "c")
print(original)  # ['a', 'b']
print(updated)   # ['a', 'b', 'c']

reset(original)
print(original)  # ['a', 'b']

add_item() 修改共享列表,调用方会观察到变化。add_item_copy() 返回新列表,更适合需要可预测数据流的代码。reset() 只让局部变量指向新列表,并没有清空调用方的对象。

这也是不应使用可变默认参数的重要原因:默认值通常在函数定义时创建,并被后续调用重复使用。

def collect_bad(item, bucket=[]):
    bucket.append(item)
    return bucket


def collect(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

print(collect_bad("a"))  # ['a']
print(collect_bad("b"))  # ['a', 'b']

print(collect("a"))      # ['a']
print(collect("b"))      # ['b']

自定义类如何表达可变性

普通类的属性通常可以修改,因此实例默认表现为可变对象。需要值对象时,可以使用冻结的数据类明确表达“创建后不应修改”的约束。

from dataclasses import dataclass


@dataclass
class Cart:
    items: list[str]

    def add(self, item: str) -> None:
        self.items.append(item)


@dataclass(frozen=True)
class Coordinate:
    x: int
    y: int


cart = Cart([])
cart.add("keyboard")
print(cart)  # Cart(items=['keyboard'])

point = Coordinate(10, 20)
print(point)

try:
    point.x = 99
except Exception as exc:
    print(type(exc).__name__, exc)

frozen=True 会阻止属性重新赋值,但它不是递归冻结。如果冻结数据类包含列表,列表自身仍然可以被修改。要构造更可靠的不可变值对象,应优先使用整数、字符串、元组以及其他不可变成员。

此外,不要轻易让可变实例充当字典键。自定义 __eq__()__hash__() 时尤其要保证:对象参与相等性判断的状态,在作为键期间不能改变。

选择时看语义,而不只是性能

可以按照以下原则检查代码:

  • 数据需要原地累积、更新或删除时,使用列表、字典、集合或明确设计的可变类。
  • 数据代表坐标、配置快照、复合键等固定值时,考虑元组或冻结数据类。
  • 函数会修改传入对象时,通过命名、文档和返回值明确说明副作用。
  • 需要隔离修改时,创建副本;同时注意浅复制不会复制内部嵌套对象。
  • 字典键和集合成员必须可哈希,并且其哈希相关状态应保持稳定。
  • 避免可变默认参数,使用 None 并在函数内部创建新容器。

可变对象适合表达持续变化的状态,不可变对象适合表达稳定的值。真正需要掌握的并不是哪一类“更好”,而是一次操作究竟修改了哪个对象,以及这次修改会被哪些引用观察到。


相关推荐