Python 中 append、insert 和 extend 有什么区别?
线上脚本跑完,数据少了 1 行。
不是接口丢了,也不是文件少了。我第一眼看代码,问题就卡在这一句:
rows = rows.append(row)
这种写法我见过太多次了。写 Java 写习惯的人,容易下意识觉得 append 会返回新列表。Python 不惯着你,它直接返回 None。
跑一下就明白:
rows = []ret = rows.append({"user_id": 1001, "amount": 39.9})
print(rows)
print(ret)
输出是:
[{'user_id': 1001, 'amount': 39.9}]
None
append 做的事情很简单:把一个元素塞到列表尾部,改的是原列表。
注意,是一个元素。
order_ids = [1001, 1002]order_ids.append([1003, 1004])
print(order_ids)
结果是:
[1001, 1002, [1003, 1004]]
这里 [1003, 1004] 没有被拆开,它作为一个整体被塞进去了。
这地方我一般会多看一眼,因为很多脏数据就是这么混进去的。本来后面代码以为每个元素都是订单号,结果突然来了一个 list,日志里就开始报这种东西:
TypeError: unhashable type: 'list'
如果你想把另一个列表里的元素一个个追加进去,用的是 extend。
order_ids = [1001, 1002]order_ids.extend([1003, 1004])
print(order_ids)
结果:
[1001, 1002, 1003, 1004]
append 和 extend 的区别,别背概念,就记这个现场:
a = ["A"]
a.append(["B", "C"])
print(a)b = ["A"]
b.extend(["B", "C"])
print(b)
输出:
['A', ['B', 'C']]
['A', 'B', 'C']
一个是把箱子塞进去,一个是把箱子里的东西倒进去。
但 extend 也有坑。
字符串也是可迭代对象,所以它会被拆开。
tags = ["paid"]tags.extend("vip")
print(tags)
输出:
['paid', 'v', 'i', 'p']
这代码我看着就不太放心。业务上你大概率是想加一个标签,而不是加三个字符。
应该这么写:
tags = ["paid"]tags.append("vip")
print(tags)
结果才是:
['paid', 'vip']
再说 insert。
insert 是往指定位置插入元素。
steps = ["check_stock", "create_order", "send_msg"]steps.insert(1, "lock_coupon")
print(steps)
输出:
['check_stock', 'lock_coupon', 'create_order', 'send_msg']
看起来挺顺手,但我在线上代码里不喜欢大量用它。
原因很直接:插在中间,后面的元素都要往后挪。列表短的时候没感觉,列表长了就有成本。尤其是那种处理几万行 CSV、日志清洗、账单明细重排的脚本,循环里频繁 insert(0, item),性能很容易难看。
比如这种写法,我一般会皱眉:
lines = []for record in records:
if record["level"] == "ERROR":
lines.insert(0, record)
else:
lines.append(record)
它的意图是把错误日志放前面。写是能写,数据量一上来就别扭。
我更愿意拆成两个列表,最后再拼:
error_lines = []
normal_lines = []for record in records:
if record["level"] == "ERROR":
error_lines.append(record)
else:
normal_lines.append(record)
lines = error_lines + normal_lines
这段代码没那么“炫”,但排查起来舒服。哪批数据进了错误区,哪批进了普通区,一眼能看出来。
还有一个小细节,insert 的位置超了也不会报错。
nums = [1, 2, 3]nums.insert(99, 4)
print(nums)
nums.insert(-99, 0)
print(nums)
结果:
[1, 2, 3, 4]
[0, 1, 2, 3, 4]
索引太大,就插到最后。索引太小,就插到最前。
这个行为有时候挺方便,有时候也会掩盖问题。比如你算出来的插入位置本来不该是 99,但程序没报错,数据顺序悄悄变了。后面再查就烦。
我自己写批处理脚本时,经常会加一层很土的判断:
definsert_before_submit(flow, new_step, before_step):
try:
idx = flow.index(before_step)
except ValueError:
raise RuntimeError(f"流程缺少节点: {before_step}") flow.insert(idx, new_step)
return flow
flow = ["check_user", "submit_order", "notify"]
insert_before_submit(flow, "check_risk", "submit_order")
print(flow)
输出:
['check_user', 'check_risk', 'submit_order', 'notify']
这里我没有直接 insert(1, "check_risk"),因为业务流程这种东西,写死位置不稳。哪天前面多一个节点,位置就变了。按节点名找位置,至少出错时能炸得明白。
再补一个经常被忽略的点:这三个方法都会修改原列表,并且都返回 None。
items = [1, 2]print(items.append(3))
print(items.insert(0, 0))
print(items.extend([4, 5]))
print(items)
输出:
None
None
None
[0, 1, 2, 3, 4, 5]
所以别写这种:
items = items.extend(new_items)
写完 items 就变成 None 了。后面再来一行:
items.append(x)
就会报:
AttributeError: 'NoneType' object has no attribute 'append'
这种错误日志不用怀疑数据库,不用怀疑环境,先搜前面有没有把列表方法的返回值重新赋给自己。
最后按我平时写代码的习惯收一下:
追加一个元素,用 append。
追加一批元素,用 extend,但小心字符串会被拆开。
往指定位置塞一个元素,用 insert,但不要在大循环里频繁往头部或中间插。
别接它们的返回值。
Python 的 list 就这点脾气。它改原对象,不给你新对象。记住这个,很多莫名其妙的 NoneType 问题能少掉一半。