前言
sorted()是使用频率最高的内置函数之一,但它的能力常被低估。多数人只用到sorted(numbers),实际上它还能做多级排序、按对象属性排序、按任意计算结果排序,并且有一条被广泛误解的性质——稳定性(stability)。
先纠正几个高频错误说法:
- 「
sorted()会原地排序」——不会。它返回一个新列表,原对象不动。真正原地排序的是list.sort()。 - 「Python 3 里可以用
cmp=参数传比较函数」——没有这个参数。cmp是 Python 2 的东西,Python 2.7 已于2020 年 1 月 1 日停止维护(PEP 373);Python 3 只接受key,老式的比较函数要用functools.cmp_to_key()转换。 - 「
reverse=True只是把结果倒过来」——不完全对。它是「按相反的比较顺序排序」,和「排完再倒序」在稳定性上的表现不同(见下文)。
官方签名是:
sorted(iterable, /, *, key=None, reverse=False)注意那个单独的*:key和reverse都只能以关键字形式传,写成sorted(data, str.lower)会直接TypeError。这是很多人第一次用key时踩的坑。
一、基本用法与返回值
# 适用于 Python 3.8+
nums = [3, 1, 2]
out = sorted(nums)
print(out) # [1, 2, 3]
print(nums) # [3, 1, 2] —— 原列表不变
print(sorted(nums) is nums) # False,是新列表
# 对字符串排序得到的是「字符列表」
print(sorted("bca")) # ['a', 'b', 'c']
# 对字典排序,得到的是排好序的键列表
print(sorted({"b": 2, "a": 1})) # ['a', 'b']对字典直接排序得到的是键,因为迭代字典得到的是键。要按键值排序得显式指定,例如sorted(d.items(), key=lambda kv: kv[1])。
排序使用<来比较元素。如果元素之间不可比较(比如int和str混在一列里),会抛TypeError——这一点和「能比较就能排序」的直觉一致,但混类型列表在真实数据里很常见,处理前最好先清洗。
二、key 与 reverse
key接受一个单参数函数,sorted会对每个元素调用它,用返回的「排序键」来比较,而元素本身原封不动地保留在结果里。官方文档对key的描述就是这个意思:提取用于比较的键,默认None表示直接比较元素。
# 适用于 Python 3.8+
words = ["Python", "go", "JavaScript", "c"]
print(sorted(words, key=len)) # ['c', 'go', 'Python', 'JavaScript']
print(sorted(words, key=str.lower)) # 按小写形式比较
data = [("b", 2), ("a", 3), ("c", 1)]
print(sorted(data, key=lambda t: t[1])) # [('c', 1), ('b', 2), ('a', 3)]reverse=True是「按相反顺序比较」,而不是「排完再反转列表」:
# 适用于 Python 3.8+
print(sorted([1, 3, 2], reverse=True)) # [3, 2, 1]那两者什么时候不一样?当存在比较结果相同(相等)的元素时。sorted()保证稳定:相等元素的相对顺序保持原样。reverse=True是「把比较反转」,所以相等元素仍然保持原来的相对顺序;而「先升序排好、再整体reverse()」会把相等元素也颠倒过来。这个差别在处理「主键相同、次序有意义」的数据时会真的显现出来。
三、稳定性与多级排序
官方文档明确写道:内置的sorted()保证稳定(stable)。稳定的定义是——不改变比较结果相等的元素之间的相对顺序。
稳定性带来一个非常好用的技巧:要按多个键排序,可以从次要键到主要键分多次排序。
# 适用于 Python 3.8+
records = [
("销售部", "张三", 9000),
("技术部", "李四", 12000),
("销售部", "王五", 8000),
("技术部", "赵六", 12000),
]
# 目标:先按部门(升序),部门相同再按工资(降序)
step1 = sorted(records, key=lambda r: r[2], reverse=True) # 先排次要键:工资
step2 = sorted(step1, key=lambda r: r[0]) # 再排主要键:部门(稳定)
for row in step2:
print(row)因为第二次排序是稳定的,部门相同的那些行会保持第一次排序定下的工资顺序。顺序不能反:先按主键、再按次键,会把主键的顺序打乱。
更省事的写法是把多个键拼成元组,一次排完:
# 适用于 Python 3.8+
# 部门升序,工资降序:工资取负数,把「降序」变成「升序」
rows = sorted(records, key=lambda r: (r[0], -r[2]))
for row in rows:
print(row)元组比较是逐项进行的:先比第一个元素,相同再比第二个。所以(r[0], -r[2])恰好表达了「部门升序 + 工资降序」。用负数表示降序是个常用技巧,但它只适用于数值;对字符串等非数值类型,要么分两趟排(利用稳定性),要么改用reverse配合稳定性的组合。
四、itemgetter、attrgetter 与 cmp_to_key
当排序键是「取下标」或「取属性」时,operator模块提供了比lambda更直接的替代品:
| 工具 | 作用 | 等价于 |
|---|
operator.itemgetter(2) | 取r[2] | lambda r: r[2] |
operator.itemgetter(2, 5) | 取(r[2], r[5]) | lambda r: (r[2], r[5]) |
operator.attrgetter("name") | 取b.name | lambda b: b.name |
operator.attrgetter("name.first") | 取嵌套属性 | lambda b: b.name.first |
# 适用于 Python 3.8+
from operator import attrgetter, itemgetter
rows = [("销售部", "张三", 9000), ("技术部", "李四", 12000)]
print(sorted(rows, key=itemgetter(2))) # 按第 3 列排序
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def __repr__(self):
return f"Employee({self.name!r}, {self.salary})"
staff = [Employee("张三", 9000), Employee("李四", 12000)]
print(sorted(staff, key=attrgetter("salary")))
print(sorted(staff, key=attrgetter("salary", "name"))) # 先工资后姓名attrgetter支持点号访问嵌套属性(如"name.first"),itemgetter支持字典的键、列表的索引、字符串的切片——因为它调用的就是操作数的__getitem__。
如果手上真的有一个 Python 2 时代的比较函数(接收两个参数,返回负数/零/正数),Python 3 的sorted()不能直接用它。官方给的迁移工具是functools.cmp_to_key():
# 适用于 Python 3.8+
from functools import cmp_to_key
def by_abs(a, b):
"""老式比较函数:按绝对值比较两个数。"""
return (abs(a) > abs(b)) - (abs(a) < abs(b))
print(sorted([-5, 3, -1, 4], key=cmp_to_key(by_abs))) # [-1, 3, 4, -5]写法要点是:比较函数返回负数表示「小于」,返回 0 表示「相等」,返回正数表示「大于」。cmp_to_key把它包装成key能用的对象。注意这种包装比直接用key慢,因为它要做 O(n log n) 次成对比较,而key只对每个元素计算一次——能改成key就改成key,例如上面这个例子直接写key=abs就够。
五、sorted() 与 list.sort() 的区别
这两个名字长得像,行为差在关键处:
| 对比项 | sorted(iterable, ...) | list.sort(...) |
|---|
| 作用于 | 任意可迭代对象 | 只能是列表 |
| 返回值 | 新列表 | None(原地排序) |
| 原对象 | 不变 | 被就地修改 |
| 参数 | key、reverse | key、reverse |
| 能不能对生成器用 | 能(会消费它) | 不能 |
# 适用于 Python 3.8+
nums = [3, 1, 2]
result = nums.sort() # 原地排序
print(nums) # [1, 2, 3]
print(result) # None —— 这就是坑「sort()返回None」是最经典的一类 bug:写nums = nums.sort()之后,nums变成了None,后面所有操作都报AttributeError: 'NoneType' object has no attribute ...。记住「要新列表用sorted(),要原地改且不在乎返回值才用.sort()」就不会中招。
常见坑点
1. 把list.sort()的返回值赋给变量。
❌nums = nums.sort()——nums变成None。 ✅nums.sort()(原地)或nums = sorted(nums)(新列表)。
2.key写成位置参数。
❌sorted(words, str.lower)——TypeError,因为key是 keyword-only。 ✅sorted(words, key=str.lower)。
3. 多级排序顺序搞反。
❌ 先按主键排,再按次键排 —— 第二次排序会把主键顺序打乱。 ✅ 从次要键排到主要键;或者一次用元组键key=lambda r: (r[0], r[1])。
4. 以为reverse=True就是把结果倒过来。
❌ 认为sorted(x, reverse=True)等于sorted(x).reverse()—— 对相等元素,前者保持原顺序,后者会颠倒。 ✅ 需要「主键降序、相等项保持原序」时,直接一次性用reverse=True,不要排完再倒。
5.key函数对部分元素返回None。
❌key=lambda d: d.get("score")—— 缺字段时返回None,和数字比较就TypeError。 ✅ 给缺省值:key=lambda d: d.get("score", float("-inf")),或先过滤掉缺字段的行。
6. 混类型数据直接排序。
❌sorted([1, "2", None])——TypeError: '<' not supported between instances of ...。 ✅ 排序前统一类型,或用key=str这样的转换让它们可比(但要清楚这是按字符串比)。
7. 想用 Python 2 的cmp参数。
❌sorted(data, cmp=my_func)—— Python 3 的sorted没有cmp参数,直接TypeError。 ✅ 改成key函数;实在要沿用老比较函数,用functools.cmp_to_key()包一层。
8. 对生成器排序后又去遍历它。
❌s = sorted(gen)之后再for x in gen—— 生成器已被排序过程消费,第二次是空的。 ✅ 需要留着原始数据就先把生成器list()出来,再对列表排序。
总结
| 需求 | 写法 |
|---|
| 得到新列表 | sorted(data) |
| 原地排序 | data.sort()(返回None) |
| 按计算值排序 | sorted(data, key=len)(key只能关键字传) |
| 降序 | sorted(data, reverse=True) |
| 多级排序 | 元组键key=lambda r: (r[0], r[1]),或利用稳定性从次键排到主键 |
| 按下标 / 属性 | operator.itemgetter/operator.attrgetter |
| 复用 Python 2 的比较函数 | functools.cmp_to_key(func) |
| 数字降序但保持稳定 | 拼元组时对数值取负,或直接用reverse=True |
sorted()的强大不在于参数多,而在于两条设计:key把「比较什么」和「怎么比较」解耦,稳定性让多次排序可以叠加。把这两条想明白,多级排序就不需要写复杂的比较函数,一个元组键或者两趟排序就能表达清楚。而它和list.sort()的差别只有一句话值得背下来——一个返回新列表,一个返回None。