一篇把「key 到底改什么」「单维降序怎么写」「filter 和列表推导式选哪个」讲透的实战笔记。
本文所有代码输出均为真机实测,环境:macos(apple silicon)/ python 3.11.9。
引言
python 的 sorted 和 filter 是每个开发者每天都在用的工具,但恰恰因为太常用,几个细节长期被"想当然"地误解:
sorted的key参数,很多人以为它会改变元素本身;- 想对某一维做降序,很多人第一反应是
reverse=true,结果把其他维度也一起翻了过去; filter(lambda ...)与列表推导式看起来只是风格差异,实际上返回值类型、内存行为、性能都不同。
这三件事看似零散,背后其实是同一条主线:python 的可迭代处理始终遵循"升序比较 + 稳定排序"这一底层约定,所有花哨写法都是在这条约定上做变换。搞清楚这条主线,上面三个问题会同时豁然开朗。
一、key是"比较依据",不是"元素变换"
1.1 从一个经典表达式说起
sorted(range(-5, 6), key=lambda x: x * x) # [0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]
拆开看:
range(-5, 6)生成-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5(左闭右开,含 -5 不含 6);key=lambda x: x * x指定排序依据为平方值,等价于按绝对值大小排序;- 结果是按平方值升序排列,但输出元素仍是原始值。
这里最容易出错的一点:
key 函数只负责"算出拿什么去比较",它的返回值不参与输出。
sorted 拿 key(x) 的结果去比大小,但往结果列表里放的始终是原始的 x。所以上面结果里的 -1、-2 都还在,没有被替换成 1、4。
1.2 为什么负数排在正数前面:稳定排序
按平方值看,-1 和 1 的 key 都是 1,属于"并列"。那么谁排前?
答案是 -1 排前。原因在于 python 的排序是稳定排序(stable sort):
当两个元素的 key 相等时,保持它们在原序列中的先后顺序。
原序列 range(-5, 6) 中,-1 出现在 1 之前,因此排序后 -1 依然在 1 之前。-2 与 2、-3 与 3 等同理。
完整对照如下:
| 元素 | key = x² | 结果位置 |
|---|---|---|
| 0 | 0 | 1 |
| -1 | 1 | 2 |
| 1 | 1 | 3 |
| -2 | 4 | 4 |
| 2 | 4 | 5 |
| -3 | 9 | 6 |
| 3 | 9 | 7 |
| -4 | 16 | 8 |
| 4 | 16 | 9 |
| -5 | 25 | 10 |
| 5 | 25 | 11 |
一句话总结:key 决定"比什么",稳定排序决定"同分时谁在前"。这段代码的语义就是"按绝对值升序,同绝对值时负数优先"。
二、单维降序:取负号技巧
2.1 元组 key 的比较规则
如果要让"同平方值内,正数排在负数前面",可以写成元组 key:
sorted(range(-5, 6), key=lambda x: (x * x, -x)) # [0, 1, -1, 2, -2, 3, -3, 4, -4, 5, -5]
元组 key 的比较遵循字典序:先比第一项,第一项相等才比第二项;而且始终是升序比较(sorted 默认 reverse=false)。
- 第一维
x * x:把元素按绝对值从小到大分组; - 第二维
-x:只在同组内部决定顺序。
2.2-x为什么能实现降序
关键在于:第二维也走升序比较,但它的值是取过负号的——原来的大数取负后变小,原来的小数取负后变大,排序结果自然就反过来了。
以 x = -5 与 x = 5 为例:
| 元素 | 第一维 x² | 第二维 -x | 谁在前 |
|---|---|---|---|
| -5 | 25 | 5 | |
| 5 | 25 | -5 | ✅ |
第一维打平(都是 25),比第二维:-5 < 5,所以元素 5 排在 -5 前面。
推广到所有分组:
| 组内元素 | 第二维 -x | 升序后 | 等效于原值 |
|---|---|---|---|
| 1, -1 | -1, 1 | -1 在前 | 1 在 -1 前 |
| 2, -2 | -2, 2 | -2 在前 | 2 在 -2 前 |
| 3, -3 | -3, 3 | -3 在前 | 3 在 -3 前 |
所以 -x 不是"降序开关",而是"对某一维单独取负,使其在升序比较下表现为降序"的常用写法。
2.3reverse=true是全局的,而且会抵消第二维
sorted 的 reverse=true 是对整个排序结果生效,不是针对某一维。来看实测:
sorted(range(-5, 6), key=lambda x: x * x, reverse=true) # [-5, 5, -4, 4, -3, 3, -2, 2, -1, 1, 0] sorted(range(-5, 6), key=lambda x: (x * x, -x), reverse=true) # [-5, 5, -4, 4, -3, 3, -2, 2, -1, 1, 0]
注意这两行结果完全相同。 原因很有教学意义:(x * x, -x) 的升序结果是 [0, 1, -1, 2, -2, ..., 5, -5],整体反转后变成 [-5, 5, -4, 4, ...]——第二维辛苦制造的"组内降序"被反转移回成了升序。同时第一维也被反成了降序。
这就是 reverse=true 的陷阱:它是"全翻",不是"单维控制"。想只要某一维降序,就别用它。
2.4 补充:reverse=true依然保持稳定性
一个常被问到的问题:reverse=true 会不会破坏稳定排序?答案是不会,python 文档明确保证 reverse 参数仍然维持排序稳定性:
sorted([(1, 'a'), (1, 'b'), (0, 'c')], key=lambda t: t[0]) # [(0, 'c'), (1, 'a'), (1, 'b')] sorted([(1, 'a'), (1, 'b'), (0, 'c')], key=lambda t: t[0], reverse=true) # [(1, 'a'), (1, 'b'), (0, 'c')] ← 两个 key=1 的元素仍是 'a' 在 'b' 前
2.5 陷阱:非数值维度不能取负
取负号只对数值类型有效。字符串维度会直接抛错:
sorted(['b', 'a'], key=lambda s: -s) # typeerror: bad operand type for unary -: 'str'
替代方案:利用稳定排序做"多轮排序"。 先按次要维度排,再按主要维度排(此时 reverse=true 作用在主要维度上是安全的):
data = [(3, 1), (1, 2), (3, 2), (1, 1)] data.sort(key=lambda t: t[1]) # 先排次要维度:升序 data.sort(key=lambda t: t[0], reverse=true) # 再排主要维度:降序 print(data) # [(3, 1), (3, 2), (1, 1), (1, 2)] # ↑ 第一维 3 组在前(降序),组内第二维 1 < 2 (升序)
多轮排序之所以成立,正是因为稳定排序会保留上一轮的相对顺序。这是官方推荐构建复杂排序的标准手法。
三、filter+ 列表推导式:等价但不相等
3.1 功能等价
list(filter(lambda x: x % 2 == 0, range(16))) # [0, 2, 4, 6, 8, 10, 12, 14] [x for x in range(16) if x % 2 == 0] # [0, 2, 4, 6, 8, 10, 12, 14]
两者结果完全一致。但差异藏在下面几个维度里。
3.2 差异一:返回值类型
这是最容易踩的坑:
f = filter(lambda x: x % 2 == 0, range(16)) print(f) # <filter object at 0x1043db430> ← 不是列表 print(list(f)) # [0, 2, 4, 6, 8, 10, 12, 14]
python 3 中 filter 返回的是迭代器(惰性求值),必须用 list() 物化才能得到列表。而列表推导式直接返回列表。
迭代器还有个关键特性:只能消费一次。
f = filter(lambda x: x % 2 == 0, range(16)) list(f) # [0, 2, 4, 6, 8, 10, 12, 14] list(f) # [] ← 已被耗尽,第二次取是空的
列表则没有这个问题,可以反复读取、索引、切片。
3.3 差异二:性能
filter 的瓶颈在于每一个元素都要回调一次 python 层的 lambda 函数(函数调用 + 栈帧开销很大);列表推导式的循环体由解释器在 c 层驱动,省掉了这部分开销。
实测(10000 轮,每轮处理 1000 个元素,均用 list() 强制物化以保证公平):
| 写法 | 总耗时 |
|---|---|
list(filter(lambda x: x % 2 == 0, range(1000))) | 0.2982 s |
[x for x in range(1000) if x % 2 == 0] | 0.1759 s |
列表推导式约为 filter + lambda 的 1.69 倍速。 数据量越大、轮次越多,差距越明显。
注意:如果谓词是内置的 c 实现函数(如
str.isdigit),filter反而更快,因为它无需构造 lambda、直接在 c 层调用。见 3.5。
3.4 差异三:filter的独门用法——过滤假值
filter 第一个参数传 none 时,会直接过滤掉所有假值(0、0.0、''、none、[]、false 等),无需写 lambda:
list(filter(none, [0, 1, '', 'a', none, [], [1], 0.0, false, true])) # [1, 'a', [1], true]
这一招在清洗数据时非常好用——比如去掉空行、去掉 0 值、去掉空列表。列表推导式当然也能写 [x for x in data if x],但 filter(none, data) 语义更直白:“把假的都扔掉”。
3.5 差异四:可扩展性与具名函数
筛选 + 变换场景下,列表推导式一步到位,filter 还得再套 map:
[x * 2 for x in range(10) if x % 2 == 0] # 推导式:一步完成 list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, range(10)))) # 函数式:两层嵌套
但如果谓词已经是现成的具名函数,filter 的写法反而干净利落:
list(filter(str.isdigit, ['1', 'a', '2', '', 'b3'])) # ['1', '2']
注意 'b3' 被整体判定为非数字而滤掉——因为 str.isdigit 要求整个字符串都由数字组成。
3.6 选型建议
| 场景 | 推荐写法 |
|---|---|
| 单纯筛选,且后续要用列表 | 列表推导式 |
| 筛选同时还要变换元素 | 列表推导式 |
| 数据量极大、内存敏感、逐步消费 | filter 返回的迭代器 |
| 过滤假值(空/零/none) | filter(none, seq) |
| 已有具名函数作谓词 | filter(str.isdigit, seq) |
四、速查表
| 需求 | 写法 |
|---|---|
| 按绝对值升序 | sorted(seq, key=abs) |
| 同绝对值时负数优先 | sorted(seq, key=lambda x: x * x) |
| 同绝对值时正数优先 | sorted(seq, key=lambda x: (x * x, -x)) |
| 单一数值维度降序 | sorted(seq, key=lambda x: -x) |
| 单一字符串维度降序 | sorted(seq, key=lambda x: x, reverse=true) |
| 多条件:a 降序 + b 升序 | 先 sort(key=b),再 sort(key=a, reverse=true) |
| 过滤假值 | filter(none, seq) |
| 筛选 + 变换 | 列表推导式 [f(x) for x in seq if cond(x)] |
三条核心原则:
key只产生比较依据,不改变输出元素;- 排序始终升序比较 + 稳定排序,
-x与多轮排序都是在这条约定上做文章,而reverse=true是全局开关; filter返回惰性迭代器(快在省内存、慢在 lambda 回调),列表推导式返回列表(快、直观、更常用)。
附:完整验证代码
以下脚本可一次性复现本文所有输出:
# -*- coding: utf-8 -*-
import timeit, sys
print("python:", sys.version.split()[0])
print("=== sorted key 的几种写法 ===")
print("key=x*x :", sorted(range(-5, 6), key=lambda x: x * x))
print("key=(x*x,-x) :", sorted(range(-5, 6), key=lambda x: (x * x, -x)))
print("key=x*x,rev=true :", sorted(range(-5, 6), key=lambda x: x * x, reverse=true))
print("key=(x*x,-x),rev :", sorted(range(-5, 6), key=lambda x: (x * x, -x), reverse=true))
print("=== reverse=true 是否保持稳定性 ===")
print("升序 :", sorted([(1, 'a'), (1, 'b'), (0, 'c')], key=lambda t: t[0]))
print("降序 :", sorted([(1, 'a'), (1, 'b'), (0, 'c')], key=lambda t: t[0], reverse=true))
print("=== 双轮稳定排序 ===")
data = [(3, 1), (1, 2), (3, 2), (1, 1)]
data.sort(key=lambda t: t[1])
data.sort(key=lambda t: t[0], reverse=true)
print(data)
print("=== 字符串维度不能取负 ===")
try:
sorted(['b', 'a'], key=lambda s: -s)
except typeerror as e:
print("typeerror:", e)
print("=== filter vs 列表推导式 ===")
f = filter(lambda x: x % 2 == 0, range(16))
print("filter 对象 :", type(f), f)
print("首次 list() :", list(f))
print("二次 list() :", list(f))
print("列表推导式 :", [x for x in range(16) if x % 2 == 0])
print("=== filter(none, ...) 过滤假值 ===")
print(list(filter(none, [0, 1, '', 'a', none, [], [1], 0.0, false, true])))
print("=== timeit 性能对比 ===")
n = 10000
t1 = timeit.timeit(lambda: list(filter(lambda x: x % 2 == 0, range(1000))), number=n)
t2 = timeit.timeit(lambda: [x for x in range(1000) if x % 2 == 0], number=n)
print(f"filter+lambda : {t1:.4f}s")
print(f"列表推导式 : {t2:.4f}s")
print(f"倍数比: {t1 / t2:.2f}x")
print("=== 具名函数作谓词 ===")
print(list(filter(str.isdigit, ['1', 'a', '2', '', 'b3'])))
参考:python 官方文档 sorted() / list.sort() / filter() 相关条目。
本文实测环境:macos(apple silicon)/ python 3.11.9,性能数字随机器与版本浮动,仅作量级参考。
到此这篇关于python排序与筛选惯用法之从sorted(key)到filter的性能取舍的文章就介绍到这了,更多相关python排序与筛选惯用法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论