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:continueif 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 taskswaiting_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 copiedold_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] = countcache_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 = partsreturnTrue, {"order_id": order_id,"amount": int(amount),"status": status, }ok, data = parse_line("A5001|88|paid")if ok: print(data)
这里 return True, data 返回的就是一个元组。它表达的是“这两个东西作为一组结果返回”,不是让你后面拿它当列表继续改。
列表和元组的区别,别背成面试八股。
你就盯住一个问题:这份数据后面还会不会被改。
会改,列表。
不该改,元组。
如果还要拿来做字典 key,大概率也是元组。
这个判断比“列表可变、元组不可变”更接近真实写代码时的选择。