列表里多了一个括号,后面的导入脚本就开始不对劲了。
本来想把 3 条订单明细追加进去,结果列表里塞进去一个“列表”。日志打印出来像这样:
rows = [
["A1001", "paid"],
["A1002", "paid"],
]
new_rows = [
["A1003", "refund"],
["A1004", "paid"],
]
rows.append(new_rows)
print(rows)
输出:
[
['A1001', 'paid'],
['A1002', 'paid'],
[['A1003', 'refund'], ['A1004', 'paid']]
]
这地方我第一眼就不太信。明明是追加两行,怎么第三个元素变成一个大包了?
问题就在 append。
append 很老实,它不会帮你拆东西。你给它什么,它就把什么原封不动塞到列表最后。
items = ["order_1", "order_2"]
items.append("order_3")
print(items)
结果:
['order_1', 'order_2', 'order_3']
这个没问题。
但你给它一个列表,它也当成一个元素塞进去。
items = ["order_1", "order_2"]
items.append(["order_3", "order_4"])
print(items)
结果:
['order_1', 'order_2', ['order_3', 'order_4']]
所以批量追加数据,用 append 十有八九不是你想要的。
这种场景我一般用 extend。
rows = [
["A1001", "paid"],
["A1002", "paid"],
]
new_rows = [
["A1003", "refund"],
["A1004", "paid"],
]
rows.extend(new_rows)
print(rows)
输出才正常:
[
['A1001', 'paid'],
['A1002', 'paid'],
['A1003', 'refund'],
['A1004', 'paid']
]
extend 的习惯是:你给我一个可迭代对象,我把里面的元素一个个拿出来,追加到原列表后面。
这句话别看着像概念,坑就在这里。
字符串也是可迭代对象。
tags = ["python", "list"]
tags.extend("api")
print(tags)
输出:
['python', 'list', 'a', 'p', 'i']
如果你只是想追加一个字符串,别用 extend,用 append。
tags = ["python", "list"]
tags.append("api")
print(tags)
输出:
['python', 'list', 'api']
这就是我平时判断它俩的方式:
append 是整体塞进去。
extend 是拆开塞进去。
不是谁更高级,是用途不一样。
再看 insert。
insert 不是追加,它是插队。
比如有一个待处理任务队列,正常任务都往后排,突然来一个补偿任务,要优先跑,但又不能插到最前面,因为第一个任务已经被调度器拿走了。
这种时候就会用 insert。
jobs = [
"sync_user_profile",
"push_order_status",
"clean_tmp_file",
]
jobs.insert(1, "repair_refund_status")
print(jobs)
输出:
[
'sync_user_profile',
'repair_refund_status',
'push_order_status',
'clean_tmp_file'
]
insert(index, value),第一个参数是位置,第二个参数是要插入的东西。
注意,它也是“整体插入”。
jobs = ["job_a", "job_b"]
jobs.insert(1, ["urgent_1", "urgent_2"])
print(jobs)
结果:
['job_a', ['urgent_1', 'urgent_2'], 'job_b']
别指望 insert 自动把列表拆开。它不会。
如果真要在中间插入一批元素,我一般不写循环 insert,那样看着勤快,性能和顺序都容易埋坑。
比如这样就很别扭:
jobs = ["job_a", "job_b", "job_c"]
urgent_jobs = ["fix_1", "fix_2"]
pos = 1
for job in urgent_jobs:
jobs.insert(pos, job)
pos += 1
print(jobs)
能跑,但我不太喜欢。列表中间插入,本来就要搬后面的元素,你循环插多次,就是多次搬。
更清楚一点的写法是切片:
jobs = ["job_a", "job_b", "job_c"]
urgent_jobs = ["fix_1", "fix_2"]
jobs[1:1] = urgent_jobs
print(jobs)
输出:
['job_a', 'fix_1', 'fix_2', 'job_b', 'job_c']
这段代码一眼能看出来:从下标 1 的位置,塞进去一批任务。
还有一个小坑,append、insert、extend 都是在原列表上改,不会返回新列表。
下面这种写法,线上脚本里我真见过:
order_ids = ["A1001", "A1002"]
order_ids = order_ids.append("A1003")
print(order_ids)
输出:
None
不是 Python 抽风,是 append() 的返回值就是 None。
正确写法是:
order_ids = ["A1001", "A1002"]
order_ids.append("A1003")
print(order_ids)
extend 和 insert 也是一样。
names = ["tom"]
ret = names.extend(["jerry", "lucy"])
print(names)
print(ret)
输出:
['tom', 'jerry', 'lucy']
None
这种设计其实是在提醒你:我改的是原列表,别拿返回值继续传。
平时写数据清洗脚本,我一般这么分:
单条记录进来,用 append。
bad_rows = []
for line_no, row in enumerate(raw_rows, start=1):
ifnot row.get("user_id"):
bad_rows.append({"line": line_no, "reason": "missing user_id"})
一批记录合并,用 extend。
all_rows = []
for file_name in import_files:
rows = load_rows_from_file(file_name)
all_rows.extend(rows)
指定位置补一个特殊元素,用 insert。
columns = ["user_id", "amount", "created_at"]
if"tenant_id"notin columns:
columns.insert(0, "tenant_id")
最后再把区别压一下:
append(x):把 x 当成一个元素,放到最后。
extend(xs):遍历 xs,把里面的元素一个个放到最后。
insert(i, x):把 x 当成一个元素,插到下标 i 前面。
真要记,就看这段:
data = [1, 2]
data.append([3, 4])
print(data) # [1, 2, [3, 4]]
data = [1, 2]
data.extend([3, 4])
print(data) # [1, 2, 3, 4]
data = [1, 2]
data.insert(1, [3, 4])
print(data) # [1, [3, 4], 2]
列表这三个方法不难,难的是别在批量数据、任务队列、字段拼接这种地方用顺手了。
尤其看到 append(list) 的时候,最好多看一眼。
它可能不是追加多条数据,只是在列表里塞了一个包。