Python技术迷

Python 里的列表和元组,别只记“一个能改一个不能改”

TypeError: 'tuple' object does not support item assignment

这个报错我见过太多次。代码看着不复杂,就是想改个值,结果 Python 直接把手打掉。

比如这种:

order_row = ("A1024", "paid", 168)

order_row[1] = "refunded"

一跑就炸。

问题不在赋值语法,而在 order_row 是个元组。元组创建出来以后,里面每个位置指向谁,基本就定死了。你不能把第 2 个位置从 "paid" 换成 "refunded"。

列表就不一样。

order_row = ["A1024", "paid", 168]

order_row[1] = "refunded"
print(order_row)

输出:

['A1024', 'refunded', 168]

所以很多人记住了一句话:列表可变,元组不可变。

这句话没错,但只记到这里,写业务代码还是会踩坑。

我一般看这两个东西,不先背概念,先看这个数据后面还会不会被改。

比如接口拿到一批订单,要过滤、补字段、重排顺序,这种我肯定用列表。

raw_orders = [
    {"id": "A1001", "amount": 99, "status": "paid"},
    {"id": "A1002", "amount": 0, "status": "paid"},
    {"id": "A1003", "amount": 36, "status": "cancel"},
]

valid_orders = []

for order in raw_orders:
if order["amount"] <= 0:
continue

if order["status"] != "paid":
continue

    order["checked"] = True
    valid_orders.append(order)

print(valid_orders)

这里用元组就别扭了。因为整个过程都在“整理数据”,一会儿追加,一会儿过滤,一会儿补字段。列表天生就是干这个活的。

但有些数据,我第一眼就不希望它被改。

比如一个接口允许导出的字段顺序:

EXPORT_FIELDS = ("order_id", "user_id", "amount", "created_at")

这种我会写成元组。

不是因为元组性能高那么一点点,而是它表达了一个态度:这几个字段顺序别乱动。

后面有人写代码时,想这样改:

EXPORT_FIELDS.append("phone")

直接报错,反而是好事。早点炸,比悄悄把线上导出列改歪强。

列表还有一个常见坑,是函数里乱改外面的数据。

defadd_retry_task(tasks, order_id):
    tasks.append({"order_id": order_id, "reason": "timeout"})
return tasks


waiting_tasks = []
add_retry_task(waiting_tasks, "A2001")
add_retry_task(waiting_tasks, "A2002")

print(waiting_tasks)

这段代码能跑,但要注意,waiting_tasks 被函数内部改了。

有时候这是你想要的,有时候不是。尤其是排查线上数据重复时,我一般会先搜这种 append、extend、sort,看有没有在某个函数里偷偷改了入参。

不想让函数影响原列表,就复制一份再处理:

defbuild_retry_tasks(tasks, order_id):
    copied = list(tasks)
    copied.append({"order_id": order_id, "reason": "timeout"})
return copied


old_tasks = []
new_tasks = build_retry_tasks(old_tasks, "A3001")

print(old_tasks)
print(new_tasks)

元组因为不能改,做参数时就省心一点。

defbuild_export_sql(table_name, fields):
    field_sql = ", ".join(fields)
returnf"select {field_sql} from {table_name}"


fields = ("order_id", "amount", "status")
sql = build_export_sql("t_order", fields)

print(sql)

这类数据传来传去,我就不担心哪个函数顺手把字段列表改了。

还有一个区别,和字典 key 有关。

列表不能当字典 key。

cache = {}

query_key = ["user", 1001]
cache[query_key] = "cached result"

这段会报错:

TypeError: unhashable type: 'list'

因为列表会变。今天是 ["user", 1001],下一行可能就 append 了一个东西。Python 不敢拿这种东西做 key。

元组通常可以。

cache = {}

query_key = ("user", 1001)
cache[query_key] = {"name": "Tom", "level": 3}

print(cache[("user", 1001)])

缓存 key、坐标点、联合主键,这些场景我经常用元组。

stock_cache = {}

defcache_stock(warehouse_id, sku_id, count):
    key = (warehouse_id, sku_id)
    stock_cache[key] = count


cache_stock("WH01", "SKU-7788", 15)

print(stock_cache[("WH01", "SKU-7788")])

不过这里有个坑,元组不是“里面所有东西都不能动”。

看这段:

row = ("A4001", ["paid", "checked"])

row[1].append("exported")

print(row)

输出:

('A4001', ['paid', 'checked', 'exported'])

这地方很多人第一次看会愣一下。

元组不允许你把 row[1] 换成另一个对象,但如果 row[1] 本身是个列表,这个列表内部还是能改。

所以我一般不把可变对象塞进元组里,除非很确定自己在干什么。不然以后排查数据被谁改了,会很烦。

再说一点性能。

元组通常比列表稍微轻一点,创建也会快一点。但在大多数业务代码里,这点差异不是第一优先级。别为了省一点点内存,把后续需要频繁增删的数据写成元组,最后到处转来转去:

items = tuple(["A", "B", "C"])
items = list(items)
items.append("D")
items = tuple(items)

这种代码我看到就想改。数据明明要变,开始就用列表。

我自己的习惯很简单。

会变化的,用列表。比如接口返回集合、批量导入数据、待处理任务、日志行、过滤后的结果。

不希望变化的,用元组。比如固定配置、坐标、字典 key、多字段组合标识、函数返回的稳定结构。

比如函数返回两个值,其实就是元组:

defparse_line(line):
    parts = line.strip().split("|")
if len(parts) != 3:
returnFalse, None

    order_id, amount, status = parts
returnTrue, {
"order_id": order_id,
"amount": int(amount),
"status": status,
    }


ok, data = parse_line("A5001|88|paid")

if ok:
    print(data)

这里 return True, data 返回的就是一个元组。它表达的是“这两个东西作为一组结果返回”,不是让你后面拿它当列表继续改。

列表和元组的区别,别背成面试八股。

你就盯住一个问题:这份数据后面还会不会被改。

会改,列表。

不该改,元组。

如果还要拿来做字典 key,大概率也是元组。

这个判断比“列表可变、元组不可变”更接近真实写代码时的选择。