Python技术迷

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 问题能少掉一半。