Python sorted内置函数怎么实现自定义排序?

星野
星野
发布时间:2026-10-26| 4人浏览过
原创

在 Python 中,内置函数 sorted() 实现自定义排序的核心机制在于 key 参数。通过向 key 传入一个单参数函数(Callable),该函数会将待排序序列中的每个元素映射为一个专门用于比较的“代理键值”(Key);Python 内部底层的 Timsort 算法随后仅比较这些计算出的键值,并输出一个包含原元素、顺序调整后的全新列表。无论是处理复杂字典、对象属性、多字段组合还是忽略大小写,都可以通过合理构造 key 函数高效实现。

一、核心语法与 key 参数工作机制

sorted() 是 Python 提供的通用排序内置函数,它接受任何可迭代对象(如列表、元组、字典、集合等),并在排序完成后返回一个新的列表,原序列保持不变。其完整函数签名如下:

sorted(iterable, key=None, reverse=False)

该函数的两个可选命名参数承担着明确的分工:

  • iterable:待排序的目标可迭代对象。
  • key:自定义排序的核心。接收一个单参数可调用对象(函数、lambda 表达式或方法引用),输入为当前迭代元素,输出为可比较的键(如数字、字符串或元组)。默认为 None,表示直接比较元素本身。
  • reverse:布尔值。设为 True 时按降序排列,默认为 False(升序排列)。

为了直观理解 key 参数的运作流程,可以观察其背后的映射机制:每个元素在排序前仅调用一次 key 函数提取出对应的代理键,随后算法直接对代理键排序并还原原始元素。这种设计称为 Schwartzian 变换,避免了在多次两两比对中重复计算键值的性能损耗。

Python sorted内置函数怎么实现自定义排序?
图 1:Python sorted() 内置函数基于 key 映射与 Timsort 的四阶段执行流

二、使用 Lambda 表达式实现常用自定义排序

在日常开发中,最常见也最轻量的方式是结合 lambda 匿名函数。通过在 lambda 中指定提取规则,可以轻松完成结构体、字典或特定计算逻辑的排序。

1. 字典列表按指定键排序

当列表中包含多个字典时,直接使用 sorted() 会触发 TypeError,因为 Python 3 默认不支持字典之间的直接比较(< 操作符未在字典间定义)。通过 key=lambda x: x['key_name'] 指定具体字段即可:

students = [
    {"name": "Bob", "score": 88},
    {"name": "Alice", "score": 95},
    {"name": "David", "score": 72}
]

# 按成绩升序排列
result_asc = sorted(students, key=lambda item: item["score"])
print("成绩升序:", [s["name"] for s in result_asc])
# 示例输出:成绩升序: ['David', 'Bob', 'Alice']

# 按成绩降序排列:设置 reverse=True
result_desc = sorted(students, key=lambda item: item["score"], reverse=True)
print("成绩降序:", [s["name"] for s in result_desc])
# 示例输出:成绩降序: ['Alice', 'Bob', 'David']

2. 自定义对象按实例属性排序

对于自定义类实例,可以通过 lambda 表达式访问其成员属性:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def __repr__(self):
        return f"{self.name}({self.salary})"

staff = [Employee("Zhang", 9000), Employee("Li", 12000), Employee("Wang", 7500)]

# 按薪资由低到高排列
sorted_staff = sorted(staff, key=lambda emp: emp.salary)
print("员工排序结果:", sorted_staff)
# 示例输出:员工排序结果: [Wang(7500), Zhang(9000), Li(12000)]

3. 字符串不区分大小写与按长度排序

Python 默认按照字符的 Unicode 编码排序,大写字母排在小写字母前(例如 'Banana' < 'apple')。若要实现不区分大小写的字母排序,可以直接将字符串的内置方法 str.lower 作为 key 传入,无需显式写成 lambda:

words = ["banana", "Apple", "cherry", "Date"]

# 忽略大小写排序
print("忽略大小写:", sorted(words, key=str.lower))
# 示例输出:忽略大小写: ['Apple', 'banana', 'cherry', 'Date']

# 按字符串长度排序
print("按长度排序:", sorted(words, key=len))
# 示例输出:按长度排序: ['Date', 'Apple', 'banana', 'cherry']

三、工程推荐:使用 operator 模块提升性能与可读性

尽管 lambda 灵活性高,但如果只是纯粹提取字典键或对象属性,Python 标准库中的 operator 模块提供了由 C 语言底层实现的专门提取器:itemgetter 和 attrgetter。它们在运行频率极高、数据量庞大的场景下比 lambda 性能更优,且语法更加声明式。

from operator import itemgetter, attrgetter

# 1. 字典列表:使用 itemgetter 代替 lambda
users = [{"id": 3, "score": 85}, {"id": 1, "score": 92}, {"id": 2, "score": 85}]
sorted_users = sorted(users, key=itemgetter("score"))

# 2. 对象列表:使用 attrgetter 代替 lambda
# sorted_staff = sorted(staff, key=attrgetter("salary"))
Python sorted内置函数怎么实现自定义排序?
图 2:Python 自定义排序实施与工程验证的标准四步流程

四、多维度与复合字段自定义排序

现实场景常常需要按照多个条件复合排序。例如:“先按班级升序排序;如果班级相同,再按成绩降序排序;如果成绩仍相同,最后按年龄升序排序”。在 Python 中,实现这种多字段排序的核心是依赖元组(Tuple)的内置比较规则。

1. 元组的逐位比较特性

Python 对元组进行大小比对时,会从第一个元素开始逐个比对:

  • 如果第 1 项已经区分出大小,则直接确定整体大小关系;
  • 只有当第 1 项相等时,才会继续比对第 2 项;以此类推。

2. 同向多字段排序

当多个字段都采用同一排序方向(全升序或全降序)时,只需在 key 函数中返回一个元组,按优先级将字段依次排列:

students = [
    {"class": 2, "name": "Bob", "score": 85},
    {"class": 1, "name": "Alice", "score": 92},
    {"class": 1, "name": "Charlie", "score": 85},
    {"class": 2, "name": "David", "score": 85}
]

# 先按班级升序,班级相同时按成绩升序
res = sorted(students, key=lambda s: (s["class"], s["score"]))
for item in res:
    print(item)
# 示例输出:
# {'class': 1, 'name': 'Charlie', 'score': 85}
# {'class': 1, 'name': 'Alice', 'score': 92}
# {'class': 2, 'name': 'Bob', 'score': 85}
# {'class': 2, 'name': 'David', 'score': 85}

如果使用 operator.itemgetter,也可以直接传递多个字段名:key=itemgetter("class", "score"),效果完全一致且执行效率更高。

3. 混合升降序排序技巧

如果希望“班级升序,但成绩降序”,由于 reverse 参数是全局生效的,无法单独对某一列取反。此时可以利用以下两种核心方案:

技巧 1:数值型字段直接取相反数(-x)
对于 int 或 float 等数值类型,将其值乘以 -1(即加负号),原本数值越大取反后越小,从而在整体升序的环境下实现该单列的降序效果。
# 班级升序 (s["class"]),成绩降序 (-s["score"])
res_mixed = sorted(students, key=lambda s: (s["class"], -s["score"]))
for item in res_mixed:
    print(item)
# 示例输出:
# {'class': 1, 'name': 'Alice', 'score': 92}
# {'class': 1, 'name': 'Charlie', 'score': 85}
# {'class': 2, 'name': 'Bob', 'score': 85}
# {'class': 2, 'name': 'David', 'score': 85}
技巧 2:利用 Timsort 稳定性进行链式多轮排序
若包含非数值字段(如字符串无法取负),可以利用 Python 内置排序算法的“稳定性(Stable)”特性:先对次要关键字排序,再对主要关键字排序。因为次级排序的结果在主排序键相等时会被严格保留。
# 目标:按 name 降序,再按 class 升序
# 步骤 1:先按次要键 name 排序(降序)
step1 = sorted(students, key=lambda s: s["name"], reverse=True)
# 步骤 2:再按主要键 class 排序(升序)
final_res = sorted(step1, key=lambda s: s["class"])

五、复杂规则与传统比较器桥梁:functools.cmp_to_key

在 Python 2 中,排序允许传入 cmp(a, b) 双参数比较函数。当 a < b 时返回负数,a == b 返回 0,a > b 返回正数。然而,在 Python 3 中,cmp 参数被彻底移除。这是因为两两对比机制在每次元素比对时都要执行一次转换计算,总计算量高达 $O(N \log N)$ 次;而现代 key 机制仅需 $O(N)$ 次计算。

但是,当排序规则高度依赖两个元素之间的上下文关联,无法抽象成单个元素的独立键值时(例如经典算法题“将一组正整数拼接成最大的数字”:判断 str(a) + str(b) 与 str(b) + str(a) 的相对大小),Python 提供了 functools.cmp_to_key 作为适配器。

from functools import cmp_to_key

# 经典案例:拼接最大数
nums = [3, 30, 34, 5, 9]

def compare_combine(x, y):
    # 如果 y+x 组合字符串更大,说明 y 应该排在 x 前面(即 x 应该在后,返回 1)
    if str(x) + str(y) > str(y) + str(x):
        return -1  # 降序排列,x 优先
    elif str(x) + str(y) < str(y) + str(x):
        return 1
    else:
        return 0

# 使用 cmp_to_key 转换为合法的 key 函数
sorted_nums = sorted(nums, key=cmp_to_key(compare_combine))
print("拼接最大数排序序列:", sorted_nums)
# 示例输出:拼接最大数排序序列: [9, 5, 34, 3, 30]
print("最终最大数字:", "".join(map(str, sorted_nums)))
# 示例输出:最终最大数字: 9534330
Python sorted内置函数怎么实现自定义排序?
图 3:传统 cmp 双参数比对与现代 key 单参数映射的架构演进与桥接关系

六、避坑要点与注意事项

在使用 sorted() 实现自定义排序时,以下几个关键边界条件和行为细节极易引发线上 Bug 或性能隐患:

避坑维度 常见错误与现象 正确处理方法
原地排序 vs 产生新列表 写出 items = items.sort() 导致变量变成 None list.sort() 是列表原地方法,返回 None;内置函数 sorted(items) 总是返回全新列表,不改变原对象。
类型不可比异常 列表中混有 None 或数字与字符串,抛出 TypeError: '<' not supported 在 key 函数中做好类型归一化。例如将 None 转化为极小值或极大值:key=lambda x: (x is not None, x)。
Key 函数内的繁重操作 在 key 函数中读取文件、请求外部接口或执行深拷贝 key 函数虽然只执行 $N$ 次,但如果 $N$ 很大仍会阻塞主线程。应先在外部预处理数据,再传入纯内存键值。
多维混合排序的负号限制 对非数值类型(如字符串或日期对象)直接加负号 -x["name"] 导致 TypeError 非数值类型的反向排序应通过多轮次稳定排序分别处理,或通过 cmp_to_key 编写明确的反向逻辑。
Python sorted内置函数怎么实现自定义排序?
图 4:Python 自定义排序实现方案选型与技术路径全景决策地图

七、总结

掌握 sorted() 自定义排序的精髓可以概括为以下三条黄金准则:

  1. 单一键优先用内置与 operator:简单属性提取优先使用 itemgetter 和 attrgetter,既直观又享受底层加速;快速变换使用 lambda。
  2. 多条件排序优先用元组:通过元组打包实现多层级优先权,数值字段加负号可快速反转该列排序方向。
  3. 始终牢记返回值与稳定性:sorted() 永远创建新列表,原数据不受干扰;其内置的 Timsort 算法保证相同键元素的相对顺序严格稳定,为分步链式排序提供了坚实保障。
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。