
用 sort函数对列表排序:自定义规则与注意事项
Python 列表的 sort() 方法可以按默认顺序整理元素,也可以借助 key 参数指定排序依据。它适合需要直接调整列表顺序的场景,例如按分数排列记录、按日期排列任务,或将字符串按长度排序。
掌握 sort() 不只要记住语法,还要理解它会修改原列表、不同类型之间未必能比较,以及自定义排序函数可能遇到的异常和性能问题。本文从基础用法开始,逐步说明这些边界。
一、基本用法:直接修改列表
sort() 是列表对象的方法,默认按元素的升序排列。数字按数值顺序排列,字符串按 Unicode 字符串比较规则排列。
numbers = [8, 3, 10, 1, 5]
numbers.sort()
print(numbers)
# [1, 3, 5, 8, 10]
它会在原列表上排序,返回值是 None,不是排序后的新列表。不要把返回值赋回原变量:
numbers = [8, 3, 10]
result = numbers.sort()
print(numbers)
# [3, 8, 10]
print(result)
# None
如果希望保留原列表,使用内置函数 sorted()。它接受一个可迭代对象,返回新的列表,不会通过排序操作修改传入的列表。
original = [8, 3, 10]
ordered = sorted(original)
print(original)
# [8, 3, 10]
print(ordered)
# [3, 8, 10]
因此,选择方法时先问自己是否允许改变现有列表:允许时用 list.sort();需要保留原数据或排序对象不是列表时,可考虑 sorted()。
二、参数限制与降序排序
list.sort() 的接口可以概括为:
list.sort(*, key=None, reverse=False)
key 和 reverse 都是仅限关键字的参数,必须写出参数名。例如,items.sort(True) 会因传入位置参数而报错;正确写法是 items.sort(reverse=True)。
key:接收一个函数,用于为每个元素提取排序依据,默认是None,即直接比较元素。reverse:布尔值,默认为False,表示升序;设为True表示降序。
scores = [72, 95, 81, 66]
scores.sort(reverse=True)
print(scores)
# [95, 81, 72, 66]
参数是有限的:sort() 不接受诸如 cmp= 这样的比较函数参数,也没有内置的“忽略大小写”或“按自然数字顺序”选项。需要这些规则时,应通过 key 转换排序依据,或使用标准库提供的工具。
三、用 key 自定义排序规则
传给 key 的函数接收列表中的一个元素,并返回用于比较的值。排序时,Python 会按这些返回值排列元素,同时保留元素本身。
例如,按字符串长度排序:
words = ["pear", "fig", "banana", "plum"]
words.sort(key=len)
print(words)
# ['fig', 'pear', 'plum', 'banana']
len 是可直接传入的函数,不需要写成 key=len()。后者会在调用 sort() 时先执行 len(),但没有提供它所需的字符串参数,因此会报错。
也可以通过 lambda 从复杂对象中提取字段。以下示例按字典中的年龄升序排列:
people = [
{"name": "林", "age": 31},
{"name": "周", "age": 24},
{"name": "陈", "age": 28},
]
people.sort(key=lambda person: person["age"])
for person in people:
print(person["name"], person["age"])
# 周 24
# 陈 28
# 林 31
如果排序依据由多个字段组成,可以返回元组。元组按位置逐项比较:先比较第一项,第一项相同时再比较第二项,以此类推。
students = [
{"name": "小林", "grade": 88, "age": 17},
{"name": "小周", "grade": 92, "age": 16},
{"name": "小陈", "grade": 88, "age": 16},
]
students.sort(key=lambda s: (s["grade"], s["age"]))
for student in students:
print(student["grade"], student["age"], student["name"])
# 88 16 小陈
# 88 17 小林
# 92 16 小周
这里按成绩升序,成绩相同再按年龄升序。如果业务要求成绩从高到低、年龄从小到大,不宜简单使用 reverse=True,因为它会将整个排序方向反转。可以先按升序排次要字段,再稳定地按降序排主要字段:
students.sort(key=lambda s: s["age"])
students.sort(key=lambda s: s["grade"], reverse=True)
for student in students:
print(student["grade"], student["age"], student["name"])
# 92 16 小周
# 88 16 小陈
# 88 17 小林
这种写法依赖排序稳定性,后文会解释。对于某些数值字段,也可以用负号表示降序,例如 key=lambda s: (-s["grade"], s["age"]);不过这种方式不适用于所有类型,字段类型和可能的边界值都要考虑清楚。
四、忽略大小写、数字文本与字段缺失
字符串默认比较区分大小写,并按字符串规则逐字符比较。这意味着 "item10" 不会自动被识别为数字 10,通常会排在 "item2" 前面。若只需要忽略大小写,可以用 str.casefold 作为键:
names = ["alice", "Bob", "ALICE", "carol"]
names.sort(key=str.casefold)
print(names)
# ['alice', 'ALICE', 'Bob', 'carol']
casefold() 适合进行不区分大小写的 Unicode 文本比较;它不等于按某种语言的字典或拼音规则排序。若需要面向具体地区的语言排序,应使用适合该需求的国际化排序工具,并注意其规则与运行环境。
如果字符串中包含数字,并希望按数字大小而非字符顺序排列,可以拆分数字片段并将其转成整数。例如:
import re
def natural_key(text):
parts = re.split(r"(\\d+)", text)
return [
int(part) if part.isdigit() else part.casefold()
for part in parts
]
files = ["file10.txt", "file2.txt", "file1.txt"]
files.sort(key=natural_key)
print(files)
# ['file1.txt', 'file2.txt', 'file10.txt']
这里键中可能包含字符串和整数,但在同一位置进行比较时,输入结构应保证对应项类型兼容。对于格式不规则的数据,要先定义清楚切分规则,避免生成无法相互比较的键。
字典记录有时缺少排序字段。直接写 key=lambda x: x["age"],缺少 age 的记录会触发 KeyError。可以用 get() 明确缺省策略:
records = [
{"name": "甲", "age": 30},
{"name": "乙"},
{"name": "丙", "age": 25},
]
records.sort(key=lambda r: (r.get("age") is None, r.get("age", 0)))
print(records)
# [{'name': '丙', 'age': 25},
# {'name': '甲', 'age': 30},
# {'name': '乙'}]
键的第一部分把没有年龄的记录标记为 True,因此在升序结果中排到有年龄记录后面。缺省值不是纯粹的技术细节:应按业务需求决定缺失值排在前面还是后面,并避免用一个可能与有效值混淆的默认值。
五、稳定排序是什么,何时有用
Python 的排序是稳定的:如果两个元素的排序键相同,它们在排序后的相对顺序与排序前一致。这在按多个字段排序时很有用。例如先按姓名排序,再按部门排序:
employees = [
{"name": "小周", "department": "研发"},
{"name": "小林", "department": "销售"},
{"name": "小陈", "department": "研发"},
{"name": "小王", "department": "销售"},
]
employees.sort(key=lambda e: e["name"])
employees.sort(key=lambda e: e["department"])
for employee in employees:
print(employee["department"], employee["name"])
# 研发 小陈
# 研发 小周
# 销售 小林
# 销售 小王
第二次排序按部门排列;部门相同的记录保留第一次排序得到的姓名顺序。若字段数量不多,通常用元组键更直接;当各字段排序方向不同,或需要分阶段处理规则时,稳定排序也很实用。
稳定不代表排序键可以随意设计。如果键提取结果不符合预期、缺失值处理不一致,稳定排序只会保留原有顺序,并不会自动纠正业务规则。
六、常见边界与错误
1. 混合类型无法直接比较
在 Python 3 中,整数和字符串通常不能直接按大小比较。下面的代码会在排序时引发 TypeError:
values = [3, "10", 2]
values.sort()
如果数据本质上都是数字,只是部分以文本形式保存,应先验证并转换,而不是假设它们可以直接比较:
values = ["3", "10", "2"]
values.sort(key=int)
print(values)
# ['2', '3', '10']
若列表可能包含不能转换为整数的内容,int 会引发 ValueError。应根据输入来源决定是拒绝整批数据、跳过无效项,还是把无效项安排到特定位置;不要在没有说明的情况下静默吞掉异常。
2. 排序键必须可相互比较
每个元素的键都应能按照预期进行比较。比如有些记录的键返回整数,另一些返回字符串,排序过程中比较到不兼容的键时可能报错。即使类型相同,键的自定义比较实现也可能抛出异常或定义不合理的顺序。
检查键函数的返回值类型,是排查排序错误的有效方法。排序规则最好形成一致、可解释的顺序,尤其是键由多个字段组成时。
3. None 和 NaN 不能想当然
None 与数字通常不能直接按大小比较。如果列表中可能有 None,应像前文示例那样先把它映射到明确的排序键。
浮点数的 NaN(非数字)也有特殊比较行为:它与自身比较时不等于自身,不能把含有 NaN 的数据简单视作普通数值序列,并依赖默认排序获得符合业务直觉的结果。需要排序时,应先定义缺失或异常数值的处理政策,再将其映射到明确的键。
4. key 函数不要做无关副作用
key 函数应尽量只做确定的字段提取或转换。虽然排序会为列表中的每个元素计算一次键,但函数若依赖外部可变状态、修改正在处理的数据、执行网络请求或写入文件,结果就可能难以预测,也可能让排序变慢或中途失败。
例如,日期文本可以先规范化后排序;不建议在键函数中为每个元素重复访问远程服务。需要复杂计算时,可先生成并校验排序键,再进行排序。
5. 排序不是事务
如果键函数或元素比较过程抛出异常,不应假设原列表一定完全保持不变,也不应依赖它一定完成了某种部分排序。排序操作可能中途失败;若程序必须保留可恢复的原数据,可以先复制列表,在副本上排序,确认成功后再替换引用。
data = [5, 2, 4]
working = data.copy()
working.sort()
# 排序成功后再采用结果
data = working
print(data)
# [2, 4, 5]
另外,不要在排序进行时由其他代码并发修改同一个列表。对正在排序的列表进行修改会使结果不可依赖;CPython 对排序期间检测到的列表修改还可能抛出 ValueError。无论具体实现如何,都应通过代码结构避免这种竞争。
七、旧式比较函数与 cmp_to_key
有些排序规则习惯写成“比较两个元素并返回小于、等于或大于”的函数。list.sort() 本身不接受这种旧式比较函数作为 cmp 参数。标准库提供 functools.cmp_to_key,可以将两元素比较函数转换为 key 使用。
from functools import cmp_to_key
def compare_by_last_char(a, b):
left = a[-1]
right = b[-1]
if left < right:
return -1
if left > right:
return 1
return 0
words = ["map", "bus", "cat", "dog"]
words.sort(key=cmp_to_key(compare_by_last_char))
print(words)
# ['dog', 'map', 'cat', 'bus']
实际开发中,能用提取键表达的规则通常优先使用 key:代码更简洁,也更容易检查。只有当规则确实需要比较两个完整对象,且无法自然转换成单个排序键时,才考虑 cmp_to_key。比较函数必须保持一致的比较关系,否则结果可能不符合预期。
八、性能和使用建议
对于常见的列表排序,内置排序实现已经针对实际数据做了优化。与手写双重循环相比,sort() 通常更合适,也更不容易因算法细节产生性能问题。没有实测依据时,不必为了微小差异自行实现排序算法。
合理使用 key 还有助于避免重复计算:排序过程会先为每个元素计算一次键,再据此排列。因此,把昂贵但确定的转换放在键函数里,通常比在两两比较过程中反复计算更合适。不过,如果键函数本身开销很大,仍要考虑是否可以预处理、缓存,或缩小实际参与排序的数据量。
可以按下面的思路选择实现方式:
- 只需修改列表本身:用
list.sort()。 - 需要新列表或输入不是列表:用
sorted()。 - 按字段、长度或转换后的值排序:提供
key。 - 多字段排序:优先考虑返回元组;字段方向不同,也可利用稳定排序分步处理。
- 输入类型不可靠:先验证并规范化,明确处理缺失值、非法值和混合类型。
- 规则依赖外部数据或有副作用:先把规则与数据准备过程分开,再排序。
总结
list.sort() 的核心特点是原地修改列表、以 key 指定排序依据,并通过 reverse=True 控制整体升降序。它不接收位置参数,也不提供 cmp 参数;需要比较两个对象的旧式规则时,可以用 cmp_to_key 适配。
实际使用中,最值得提前检查的是:是否允许修改原列表,键值能否彼此比较,缺失和异常数据该排在哪里,以及排序中途失败时程序是否还能安全处理数据。把这些规则写清楚,排序代码就会更可靠,也更容易维护。








