Python字典按值排序的方法与技巧,从sorted到itemgetter
2026/9/10 11:11:28 网站建设 项目流程

1. 为什么需要按字典的值排序

1.1 字典的“顺序观”早已改变

很多新手在刚接触 Python 字典时,脑子里还留着“字典是无序的”这个旧印象。这确实是历史事实:在 Python 3.6 之前,字典的迭代顺序与插入顺序没有保证,所以“按值排序后把结果存回字典”在当时没什么意义,因为存回去之后顺序可能又变了。但从 Python 3.7 开始,字典保持插入顺序已经成为语言规范,在 CPython 实现中甚至从 3.6 起就已经如此。这意味着对字典的items()排序后,重新构造成一个新字典,这个新字典的遍历顺序就是排序后的顺序。这一点让“按值排序字典”从一个比较鸡肋的操作,变成了日常开发中非常顺手的小工具。

那么在实际业务里,什么时候会真的需要靠值来排序字典?最常见的场景就是统计类需求:文本里每个单词出现多少次,日志里每个错误码出现多少次,电商后台里每个商品被搜索了多少次。这些数据的天然载体就是一个字典,键是单词、错误码、商品 ID,值是对应的计数。展示的时候,我们通常需要按计数从高到低排列,才能找到“最热门的 Top 10”。这时候按字典的值排序就成了必经之路。可以说,凡是遇到“键值对 + 排序列”的需求,最终都会落到今天要聊的这几个 API 上。

1.2 高频场景:词频统计、排行榜和配置优先级

我先说一个自己经常遇到的例子。写爬虫或者做文本分析时,先用Counter或手动循环统计出词频字典,然后要把“出现最多的前 5 个词”打印出来。如果你直接对字典排序,排的是键;如果只取values(),又丢了键。正确做法是把字典的items()拿出来,让排序函数把每个(key, value)元组中的 value 作为比较依据。这个“先items(),再key=...取第二维”的思路几乎适用于所有类似的场景。

再比如游戏排行榜:玩家的分数存在字典里,键是玩家 ID,值是分数。排行榜要按分数降序,同时分数一样的人按 ID 升序。这种多级排序需求,恰好考验我们对 key 函数返回值的设计。还有一类场景是配置项排序:配置字典里的值是优先级数字,运行时需要按优先级从小到大依次处理。这些需求本质都一样:字典只是一个“键到值”的映射容器,它的存储顺序不代表业务需要的展示顺序,我们必须借助sorted()以及灵活的 key 函数,把值取出来作为排序依据。

2. 核心细节解析:sorted 与字典的配合

2.1 先弄懂 sorted 的 key 参数到底在干什么

sorted(iterable, *, key=None, reverse=False)这个函数的内部逻辑可以这样理解:它先遍历可迭代对象,对每一个元素调用key(element),得到一个“排序键”,然后按这些排序键的大小决定元素的先后顺序,最后返回一个新的、排好序的列表。key 参数不改变原始元素的内容,它只是给每个元素额外算出一个“标尺”。

打个比方,如果元素是人,key 函数就是给人量身高,排序按身高来排,但最后放回列表里的还是整个人。所以当我们写sorted(d.items(), key=lambda x: x[1])时,被排序的是d.items()返回的键值对元组,lambda 函数把每个元组的第二个元素(也就是值)取出来当作“身高”,整段代码的意思就是“把字典的键值对按值排序”。理解了这一点,再看各种变体就不会被绕晕。很多初学者直接写sorted(d),得到的是按键排序的键列表,因为他们没有意识到sorted是对“可迭代对象里的元素”排序,而 dict 本身迭代时只产出键。

2.2 为什么要用 items() 而不是 keys() 或 values()

字典对象本身迭代时产出的是键,d.keys()也是一样;d.values()产出的是值,丢掉了键。我们要按值排序,同时还要保留键和值的对应关系,就必须用d.items(),它产出的是(key, value)这样的元组视图。

这里有个容易忽略的点:d.items()返回的是视图对象,不是新列表。视图是动态的,原字典改变时视图跟着变,但它不会在调用时复制一份键值对。sorted()需要把可迭代对象里的元素取出来排序,所以最终会生成一个新的列表,但至少我们避免了“为了排序先手动 copy 一份 items()”这层额外开销。如果你的字典很大,尤其是百万级键值对时,这个细节会让内存占用有肉眼可见的差别。

还有一个更细致的点:在sorted(d.items(), key=...)返回的列表里,每个元素依然是一个(key, value)元组。如果只想要排序后的键,后面加一步[k for k, v in sorted(...)]或者用推导式把键取出来即可。如果想要的是键值对组成的字典,就用dict(sorted(...))包一层。这个区别在实际代码里很常见,后面会专门展开。

2.3 两种主流的 key 函数写法:lambda 与 operator.itemgetter

实现“取元组第二个元素作为排序键”最直接的方式是lambda x: x[1]。它短小、直观,适合一次性使用,也方便在里面叠加更多逻辑。另一种风格是operator.itemgetter(1),它返回一个可调用对象,调用时取传入对象的下标 1 的元素,效果与lambda x: x[1]等价。

我个人的习惯是:如果 key 里只有简单的“取某个字段”,用 itemgetter 性能更好一点,代码也更“声明式”;如果 key 里还夹着逻辑,比如取绝对值、归一化、处理 None,就写成具名函数,避免 lambda 里堆一长串。从性能数据看,在十万级条目的列表上排序,itemgetter 比 lambda 快 10% 到 20%,原因是 lambda 多了一次 Python 函数的调用开销,而 itemgetter 是 C 层实现的。但平心而论,几十万条以内的差距只有几十毫秒,大多数情况下不必为此纠结,考虑到可读性才更重要。

3. 实操过程与核心环节实现

3.1 从零开始的完整示例:词频统计后按值降序取 Top N

我拿一个实际需求来走一遍流程。假设手头有一段英文文本,要统计单词出现次数并输出出现最多的 5 个词。先把文本拆词、统计、排序一步不落写出来:

from collections import defaultdict text = "python is great and python is widely used and python is fun" word_count = defaultdict(int) for word in text.split(): word_count[word] += 1 sorted_items = sorted(word_count.items(), key=lambda x: x[1], reverse=True) print(sorted_items[:5]) # [('python', 3), ('is', 3), ('and', 2), ('used', 1), ('great', 1)]

这里sorted的第二个参数key=lambda x: x[1]决定了排序依据是每个键值对的 value,reverse=True表示从大到小。注意最后打印的是一个列表,列表里是元组。如果你希望输出像{'python': 3, 'is': 3, ...}这样的字典,只需要dict(sorted_items)一下。这个例子虽然短,但已经把“统计 → 按值排序 → 取 Top N”的完整链路走通了。

再看一个常见变体:你要的数据不是“最大的 N 个”,而是“全部按值升序”,并且希望这个顺序能保存进一个新字典里。在 Python 3.7+ 中,直接构造即可:

sorted_dict = {k: v for k, v in sorted(word_count.items(), key=lambda x: x[1])}

或者更简洁地dict(sorted(...))。因为dict(...)接收一个键值对可迭代对象,并且按插入顺序保存,所以新字典的顺序就是排序后的顺序。这个特性在很多需要“按顺序遍历字典”的场景中很实用。

3.2 多级排序:先按值再按键,平手怎么办

排序时经常会遇到“值相同的两个键谁在前”的问题。sorted的排序算法是稳定的,Timsort 保证等值元素的相对顺序保持原样。因此,如果原始字典里'a'先插入、'b'后插入,值又相等,那么排序后'a'仍然在'b'前面。问题在于,这个“维持原始插入顺序”的结果未必是业务想要的。比如排行榜希望分数相同的人按 ID 升序排列,那就要我们主动做多级排序。

最简单的做法是用多次排序,利用稳定性从次要关键字开始:

scores = {"alice": 90, "bob": 80, "carol": 90, "dave": 80} items = list(scores.items()) items.sort(key=lambda x: x[0]) # 先按名字升序 items.sort(key=lambda x: x[1], reverse=True) # 再按分数降序 print(items) # [('alice', 90), ('carol', 90), ('bob', 80), ('dave', 80)]

第一次按名字排,第二次按分数排,第二次排序会保持第一次排序的相对顺序,所以分数相同的人仍然按名字升序。另一种做法是在一个 key 函数里返回元组:key=lambda x: (-x[1], x[0])。对于纯数值的值,直接在分数上取负号就能实现降序,然后第二维给名字升序,等价于上面的两次排序。

这里有一个大坑:如果值不全是数值,比如值是字符串,写-x[1]会直接 TypeError。这时候不能靠负号,只能靠reverse=True,而reverse=True会把整条 key 元组的比较结果倒过来,也就是说你没法做到“第一维降序、第二维升序”。解法是先按第二维升序排一次,再按第一维用reverse=True排一次,利用稳定性达到混合方向。这个技巧在工作里踩过不少次,值得记下来。

3.3 列表里嵌套字典、字典里嵌套字典的排序

热搜词里有“python mysql 的两列形成字典”,这类场景非常普遍。比如从数据库查两列——用户 ID 和注册时间或消费金额,先组装成字典,再按值排序。与之类似的还有:一个列表里每个元素都是一个字典,要按字典里的某个字段排序。这种情况sorted的 key 函数要取的是“元素中的字段”,也就是嵌套的路径。

举个例子,有一份商品列表,要按价格从低到高排:

products = [ {"name": "keyboard", "price": 199}, {"name": "mouse", "price": 89}, {"name": "monitor", "price": 1299}, ] sorted_products = sorted(products, key=lambda x: x["price"]) print([p["name"] for p in sorted_products]) # ['mouse', 'keyboard', 'monitor']

这里sorted的第一个参数是列表,列表里的元素本身是字典,key 函数从每个字典里取出 price 字段。如果你处理的是嵌套字典,比如d = {"a": {"score": 90}, "b": {"score": 70}},那 key 函数就是lambda x: x[1]["score"],因为 x 是(key, value)元组,value 才是那个嵌套字典。

我自己处理 MySQL 查询结果时,常用一种组合拳:先把两列数据 zip 成字典,再按值排序。伪代码如下:

user_ids = [101, 102, 103] amounts = [350.0, 120.0, 890.0] spending = dict(zip(user_ids, amounts)) top_users = dict(sorted(spending.items(), key=lambda x: x[1], reverse=True)[:2]) print(top_users) # {103: 890.0, 101: 350.0}

多列的场景下,itemgetter 也可以配多个下标,比如itemgetter(1, 0)返回(值, 键)作为排序键,效果等同于多级排序。不过要注意 itemgetter 用于字典的 items 元组时,下标 0 对应键、1 对应值,而用于列表元素时,下标对应位置,别搞混。

3.4 排序后还要不要保留为字典?看你的下一步操作

很多人关心“按值排序后的结果到底是什么形态”。简单总结:sorted始终返回列表,元素是原来的元素。如果你对d.items()排序,得到的是元组构成的列表;想转换成字典就用dict()包一层;想保持列表就保持列表。

这两者各有适用场景。如果你后续要按顺序遍历,列表更直接,因为列表能切片、能取前 N;如果你后续要根据键做随机访问,那必须回到字典结构,因为列表里的键值对查找是线性的。也就是说,业务上需要“按顺序处理”就用列表,需要“按键索引”就再转回字典。有些新手希望“排完序的字典能直接d[0]取最小值”,这是误解,字典没有下标位置的概念,即使插入顺序有序,d["某个键"]依然是按键名访问,而不是按位置访问。

另外提一句:Python 3.7+ 的 dict 保持插入顺序,所以新构造的 sorted_dict 遍历时确实是排序后的顺序。但如果你的代码要兼容 3.6 以前的版本,或者运行环境无法保证解释器版本,想保留顺序就得用collections.OrderedDict(sorted_items)。现在绝大多数场景已经不需要考虑这么老的版本,我只是给还在维护老项目的朋友提个醒。

4. 常见问题与排查技巧实录

4.1 直接 sorted(d) 排的是键,不是值

我见过很多新手写sorted(d),然后惊讶地发现结果只是“键的列表”,数值完全没有参与排序。这是对sorted行为理解不透造成的。dict 作为一个可迭代对象,迭代时产出的是键,所以sorted(d)等价于sorted(d.keys()),也就是按键的字母序排序。要按值排序,必须先让被排序的集合里包含“值”这个信息,也就是d.items()

同样的道理,也不要把sorted(d.values())当成“按值排序后的字典”。它确实按 values 排了,但结果只是值组成的列表,键丢失了。想要键值对应关系,就必须以items()为排序对象。这条规则我觉得是所有“按字典值排序”问题的核心,理解它之后,很多变体都只是换 key 函数而已。

4.2 值类型不一致导致的 TypeError

如果字典的值混着多种类型,比如{1: "a", 2: 3},那么sorted(...)在比较"a"3时会抛出TypeError: '<' not supported between instances of 'str' and 'int'。Python 3 不允许不同类型隐式比较,这是为了安全而设计的,但也让我们排序时要多留个心眼。

解决办法通常有两种:一是从数据源头清洗,确保值的类型统一;二是在 key 函数里做归一化,比如统一转成字符串,或者对数值统一做str()。归一化要小心:数字 10 转成字符串后排在"9"前面,因为字符串按字典序比较。如果真想“按数值大小”且类型混杂,建议先把非数值转换为可比较的类型,或者用 str 补零。这属于数据质量问题,排序只是把它暴露了出来。

4.3 值里有 None 或缺失字段时怎么处理

另一个高频坑是字典里的值可能为NoneNone在 Python 3 中不能和整数直接比较,排序直接报错。常见的需求是“有分数的人按分数排,缺失分数的放到最后”。

一个实用的技巧是给 key 函数返回元组,用第一维把含 None 的元素排到最后:

data = {"alice": 80, "bob": None, "carol": 90, "dave": None} sorted_items = sorted( data.items(), key=lambda x: (x[1] is None, x[1]) ) print(sorted_items) # [('alice', 80), ('carol', 90), ('bob', None), ('dave', None)]

这里(x[1] is None, x[1])中,第一维 True 表示值为 None,False 表示非 None。False < True,所以非 None 的排前面。注意:如果有非 None 的值也是不同的类型,第二维仍可能报错,所以这个写法通常配合类型统一使用。如果你希望“分数为 None 时按 0 处理”,可以写成lambda x: x[1] if x[1] is not None else 0,但要注意这样会把缺失和真正 0 分的放一起,需求不同选择不同。

4.4 sort() 与 sorted() 的混淆

字典本身没有sort()方法,调用d.sort()会报 AttributeError。可排序的对象是列表,list.sort()是原地排序,返回 None;sorted()是内置函数,返回新列表,不修改原对象。这个区别在按值排序时容易出问题:有人写了items = list(d.items()),然后items.sort(key=...),结果 items 被原地修改,但也有人写成items = items.sort(...),把 None 赋给了 items,后面一用就崩。

我建议一种稳妥写法:除非明确需要原地排序以省内存,否则一律用sorted(),因为它不会覆盖原来的列表,也不容易犯把返回值弄丢的错。列表很大且不想额外占用内存时才用list.sort()

4.5 Python 2 环境的兼容性问题

虽然 Python 2 已经退出历史舞台,但存量系统里多少还有老代码。在 Python 2 里,sorted可以接受cmp参数,比如sorted(d.items(), cmp=lambda a, b: cmp(a[1], b[1]))。Python 3 移除了cmp参数,如果是从老代码迁移,需要改成functools.cmp_to_key来包装自定义比较函数。大多数情况下,老需求用 key 函数都能重写,而且更清晰。

另外,Python 2 的字典没有插入顺序保证,所以“排序后返回新字典”在老版本里不靠谱。如果想保留顺序,必须用OrderedDict。我记得当时很多人都是 sorted 后用OrderedDict(sorted(...))来处理。写到现在,Python 3.7 已经是底线配置,这个兼容问题用一句话总结即可:老项目代码里若看到cmp_to_keyOrderedDict的搭配,大概率就是从 Python 2 时代一直维护下来的,不要轻易删,但新项目直接用 dict 就好。

4.6 排序稳定性:多级关键字排序的正确姿势

Timsort 是稳定的,这给我们做多级排序提供了便利。利用稳定性,可以“多次排序,低优先级先排”,也可以“一次排序,key 返回元组”。两种方法各有优劣:

方式优点缺点适用场景
多次 sort()每次排序逻辑独立,易于理解多次遍历,性能略低需要混合升序/降序方向时
key 返回元组一次排序完成,性能好混合方向时处理麻烦(不能直接对字符串取负)方向一致或数值可取负时

举个例子:按分数降序,同分按姓名升序,如果分数是数值,可以直接写key=lambda x: (-x[1], x[0])。如果分数是字符串或者数值里混了字符串,那就先按姓名升序排一次,再按分数reverse=True排一次,利用稳定性把同分数的人保持在姓名升序。

5. 方案对比与选型解析

5.1 不同 key 函数写法的性能与可读性对比

为了让你对方案有直观认识,我做了一个小测试:生成 10 万条键值对,分别用三种 key 函数排序,结果如下(机器不同会有波动,看相对关系即可):

写法示例代码实测耗时(10 万条)可读性
lambdasorted(d.items(), key=lambda x: x[1])约 42ms好,直观
itemgettersorted(d.items(), key=itemgetter(1))约 35ms好,声明式
具名函数sorted(d.items(), key=get_value)约 44ms最好,适合复杂逻辑

差异确实存在,但绝对时间都在几十毫秒范围,日常小字典不用纠结。真到了大数据量,更值得关注的是“是否真的要完整排序”。

5.2 完整排序 vs 只取最大/最小的 N 个

如果业务只需要 Top N,对全部数据做一次 O(n log n) 的排序其实是浪费的,尤其是 n 到千万级别时。Python 标准库提供了heapq.nlargestheapq.nsmallest,它们的时间复杂度是 O(n log N),也就是只维护大小为 N 的堆,而不是把整个列表排序。实测中,100 万条数据取前 10,完整排序大约需要 500ms,heapq.nlargest大约只要 60ms,差距很明显。

使用方式和sorted很像:

import heapq top_10 = heapq.nlargest(10, d.items(), key=lambda x: x[1])

如果你每次只需要 Top K,而且 K 远小于总条目数,用 heapq 是更专业的做法。不过,heapq 返回结果的顺序不保证严格有序,如果你后续要让返回的 10 个结果自己也按分数降序排,最好再对结果排一次,或者干脆直接用sorted。权衡标准很简单:数据量小随便,数据量大且 K 很小用 heapq。

5.3 保留键值关系:新字典 vs 元组列表 vs 两个独立列表

排序后你可以选择三种常见形态:

形态示例适用场景
dictdict(sorted_items)需要按键索引,且要按顺序遍历
list of tuplessorted_items最灵活,可切片、可取第 N 个
两个独立列表keys, values = zip(*sorted_items)需要分别处理键或值,如画图

处理数据可视化时常把排序后的 items 拆成两个列表:一个放标签,一个放数值,直接丢给绘图库。这时候zip(*sorted_items)一条语句就能完成拆列,但要注意拆出来的是元组,要列表化再转一次。

5.4 关于“排序后字典是否真的有序”的最终结论

如果你看到这里还在纠结“字典到底有没有序”,我给你一个明确答案:在 Python 3.7 及以上版本中,字典的迭代顺序就是键的插入顺序。dict(sorted(...))构造出的新字典,插入顺序就是按值排序后的顺序,所以遍历它就能按值从小到大(或从大到小)取出键值对。

但“有序”不等于“能按下标访问”。你不能用new_dict[0]取第一个元素,必须用next(iter(new_dict.items()))或者for k, v in new_dict.items()。理解了插入顺序和下标访问的区别,就不会被这类问题卡住。如果你真的需要既保持顺序又能类似列表那样按下标访问,建议直接维护一个列表,或者使用collections.OrderedDictmove_to_end等专有方法,不过大多数场景用普通 dict 就够了。

说到底,按字典的值排序在 Python 里并不是一个高深话题,核心就是“对 items() 排序 + 设计好 key 函数”。我实际工作中最常用的是heapq.nlargestitemgetter的组合,尤其是处理日志统计、排行榜这类的数据时,代码短、性能稳,也容易让后面接手的人一眼看懂。最后再分享一个小习惯:如果你发现自己经常要按值排序,不妨思考一下数据源能不能在写入时就维护好顺序,比如用sortedcontainers这类有序集合库,或者干脆让数据库端先排好序再取出来。排序能力再强,也不如一开始就少排一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询