Python bisect 的 key 参数:记录能提取排序键,为什么搜索值还要自己先转换
任务列表里保存的是字典,按截止时间排序。给 bisect_left 配上提取截止时间的 key 函数后,你把另一个任务字典作为搜索值传进去,结果仍然抛出类型错误。容易误解的地方是,二分查找会把 key 用于列表中的元素,却不会自动把搜索值也变成键。两侧必须已经处在可比较的值域里。
查找插入位置时,应传截止时间这个键;真正调用 insort_left 插入记录时,则传完整记录。后者内部会用 key 提取新记录的键以寻找位置,然后把原记录插进去。下面用整数时刻演示两个接口的区别,保存为 demo.py,执行 python demo.py,要求 Python 三点十或更新版本。
AI生成概念插图:有序记录带着各自的圆形键标记,独立搜索标记对准中间空隙;不是程序界面或运行截图。
from bisect import bisect_left, bisect_right, insort_left
from operator import itemgetter
rows = [
{"id": "a", "due": 10},
{"id": "b", "due": 20},
{"id": "c", "due": 20},
{"id": "e", "due": 30},
]
key = itemgetter("due")
assert all(key(a) <= key(b) for a, b in zip(rows, rows[1:]))
new = {"id": "d", "due": 20}
try:
bisect_left(rows, new, key=key)
except TypeError:
print("raw record rejected")
else:
raise AssertionError("untransformed record was comparable")
left = bisect_left(rows, key(new), key=key)
right = bisect_right(rows, key(new), key=key)
assert (left, right) == (1, 3)
print("equal-key range:", left, right)
insort_left(rows, new, key=key)
assert rows[1] is new
assert [r["id"] for r in rows] == ["a", "d", "b", "c", "e"]
assert all(key(a) <= key(b) for a, b in zip(rows, rows[1:]))
print("after insert:", [r["id"] for r in rows])
missing = bisect_left(rows, 25, key=key)
assert missing == 4 and key(rows[missing]) != 25
print("missing key insertion position:", missing)查位置传键,插记录传对象
代码先故意把字典交给 bisect_left,并捕获 TypeError,打印 raw record rejected。它说明比较时遇到了整数和字典,不能据此认为 key 没有生效。正确调用把二十作为搜索值,左右边界分别是一和三,因为截止时间二十的两条记录占据这两个位置之间的区间。
接着 insort_left 接受完整的任务 d。插入后的编号是 a、d、b、c、e;新任务排在原有相同键值之前,而且它仍然是原来的字典对象。这个断言特意检查身份,避免误以为函数会把字典转换成一个整数再储存。搜索阶段使用键,列表修改阶段保留完整记录。
如果你希望同截止时间的新任务排在旧任务之后,可以选择 insort_right,但这种规则仍依赖插入历史。需要稳定的业务优先顺序时,应把截止时间和编号等字段组合成排序键,并让初始排序、查找和插入全程使用同一个定义。不要只在显示时补一个排序规则,却让插入维持另一套规则。
正确的键也需要正确的列表前提
二分算法不会替你验证整个列表是否已经有序。示例在开始与插入后分别用相邻键比较核对顺序,只是小规模演示的防护;实际频繁操作时,应在数据入口建立不变量,而不是每查一次都重新扫描整张表。先排序一次也不够,如果某条记录的截止时间随后原地改变,原来的位置可能立刻失效。
找到了插入位置也不等于找到了目标记录。对于不存在的时间,函数同样返回一个可以插入的位置;若你需要确认命中,应先检查位置未越过末尾,再比较该位置的键。对于同键的多条记录,单个位置也不能回答哪一个编号正确,这属于另一层选择条件。
性能判断也要把寻找位置和实际插入分开。二分寻找位置通常只做对数级比较,而列表中间插入仍要移动后面的元素,不能因为模块叫二分就声称整个更新操作都是对数时间。若数据不断追加且天然有序,直接追加往往更简单;频繁随机插入大集合则需要重新考虑数据结构。
最后,key 应保持确定且没有副作用,别在提取字段时顺便修改记录或读取易变状态。同一次查找中的比较规则若不断变化,结果就失去明确含义。可以把“记录是什么”“键是什么”“新对象是否完整保留”各写一个断言,这三个问题拆开后,接口之间看似不一致的行为就容易核对。
资料核对日期:2026年10月2日(北京时间)。示例在本地 Python 3.12.14 实际运行并通过断言,结果仅对应文中给定输入。


