Python的排序把我坑惨了,原来sort和sorted的区别这么大

小郑那天写了一个排行榜功能,需要把用户按积分从高到低排序,然后展示前10名。

他拿到的数据是一个列表,里面是字典,每个字典有用户名和积分。他写了一段代码:

ini 复制代码
users = [
    {"name": "Alice", "score": 85},
    {"name": "Bob", "score": 92},
    {"name": "Charlie", "score": 78},
]

users.sort(key=lambda u: u["score"], reverse=True)
print(users)

跑出来没问题,排行榜正常显示。但过了一会儿,产品经理说:"能不能在排行榜下面再加一个'所有用户按注册时间排序'的列表?"

小郑心想,简单。他直接对同一个users列表又调了一次排序:

bash 复制代码
users.sort(key=lambda u: u["name"])
print(users)

结果排行榜的数据乱了------原本按积分排好的顺序被覆盖了。他这才意识到:sort()是原地修改,排完一次,原列表的顺序就永久变了。如果想要两个不同顺序的列表,必须用sorted()生成新列表,而不是反复sort()同一个列表。

"我就想排个序,怎么还把数据搞丢了?"

问题的根源,是他没搞清楚sort和sorted的区别。这两个东西看起来都是排序,但行为完全不同。

先看一个最直观的对比

scss 复制代码
nums = [3, 1, 4, 1, 5, 9, 2, 6]

# 方式一:sort()
result1 = nums.sort()
print("sort() 之后 nums:", nums)
print("sort() 的返回值:", result1)

# 方式二:sorted()
nums2 = [3, 1, 4, 1, 5, 9, 2, 6]
result2 = sorted(nums2)
print("sorted() 之后 nums2:", nums2)
print("sorted() 的返回值:", result2)

输出:

scss 复制代码
sort() 之后 nums: [1, 1, 2, 3, 4, 5, 6, 9]
sort() 的返回值: None

sorted() 之后 nums2: [3, 1, 4, 1, 5, 9, 2, 6]
sorted() 的返回值: [1, 1, 2, 3, 4, 5, 6, 9]

区别非常明显:

sort()把原列表原地 排好序,原列表被修改,返回值是None。

sorted()不碰 原列表,返回一个新的排好序的列表。

用一个生活场景来理解。你有一摞扑克牌,想按大小整理。

sort()的做法是:直接在桌子上把这摞牌重新排好。原来的顺序没了,但你手里还是那摞牌。

sorted()的做法是:拿出另一张桌子,把这摞牌复制一份,在另一张桌子上排好。原来那摞牌纹丝不动,你手里多了一摞排好序的牌。

区别一目了然:一个改原数据,一个不改。

sort() 是列表的方法,sorted() 是内置函数

这个区别看起来是语法层面的,但它决定了两者的适用范围。

sort()是列表对象的方法,只能用于列表:

ini 复制代码
lst = [3, 1, 2]
lst.sort()  # 可以

tup = (3, 1, 2)
tup.sort()  # AttributeError: 'tuple' object has no attribute 'sort'

sorted()是Python的内置函数,接受任何可迭代对象:

scss 复制代码
sorted([3, 1, 2])        # [1, 2, 3]
sorted((3, 1, 2))        # [1, 2, 3]
sorted({3, 1, 2})        # [1, 2, 3]
sorted("cab")            # ['a', 'b', 'c']
sorted({"b": 2, "a": 1}) # ['a', 'b'],对键排序

注意:sorted()对字典排序时,默认对键排序,返回的是键的列表。

sort()只能作用于列表,sorted()可以作用于任何可迭代对象。这是两者最本质的能力差异。

为什么 sort() 返回 None?

这是Python的一个设计决策,不是bug。

sort()是原地操作,它修改了列表本身。如果它返回排序后的列表,会让人误以为原列表没变。返回None是一个明确的信号:这个操作没有产生新对象,你要的东西就在原列表里。

这个设计理念在Python里很常见。所有原地修改的方法都返回None:

scss 复制代码
lst = [1, 2, 3]
print(lst.append(4))    # None
print(lst.extend([5]))  # None
print(lst.reverse())    # None
print(lst.sort())       # None

而返回新对象的方法/函数则返回结果:

python 复制代码
print(sorted([3, 1, 2]))   # [1, 2, 3]
print(list(reversed([1, 2, 3])))  # [3, 2, 1]

这个规则叫"命令-查询分离":修改数据的操作不返回值,返回值的操作不修改数据。

所以当你写出这样的代码时:

ini 复制代码
nums = [3, 1, 2]
nums = nums.sort()
print(nums)  # None

问题不在于sort()有bug,而在于你把None赋给了nums。原列表确实排好序了,但你用None覆盖了它,把排好序的列表弄丢了。

坑一:把 sort() 的返回值赋给变量

这是最常见的错误:

ini 复制代码
nums = [3, 1, 2]
nums = nums.sort()  # nums 变成 None

正确的写法是直接调用,不接收返回值:

scss 复制代码
nums = [3, 1, 2]
nums.sort()
print(nums)  # [1, 2, 3]

或者用sorted():

scss 复制代码
nums = [3, 1, 2]
nums = sorted(nums)
print(nums)  # [1, 2, 3]

坑二:以为 sorted() 会修改原列表

反过来,有些人用了sorted(),以为原列表被改了:

scss 复制代码
nums = [3, 1, 2]
sorted(nums)
print(nums)  # [3, 1, 2],没变

sorted()返回的新列表没有被接收,直接丢弃了。原列表纹丝不动。

正确的写法是接收返回值:

scss 复制代码
nums = [3, 1, 2]
nums = sorted(nums)
print(nums)  # [1, 2, 3]

或者用sort()原地修改:

scss 复制代码
nums = [3, 1, 2]
nums.sort()
print(nums)  # [1, 2, 3]

坑三:对元组或字符串用 sort()

sort()只能用于列表。如果你有一个元组想排序,只能用sorted():

ini 复制代码
tup = (3, 1, 2)
tup.sort()  # AttributeError

result = sorted(tup)  # [1, 2, 3]

字符串也一样:

ini 复制代码
s = "cab"
s.sort()  # AttributeError

result = sorted(s)  # ['a', 'b', 'c']

注意sorted("cab")返回的是字符列表,不是字符串。如果你想要排序后的字符串,需要再拼回去:

sql 复制代码
result = "".join(sorted("cab"))
print(result)  # "abc"

坑四:在需要保留原顺序的场景用了 sort()

这是小郑踩的坑。

ini 复制代码
users = [{"name": "Alice", "score": 85}, ...]

# 按积分排序
users.sort(key=lambda u: u["score"], reverse=True)
leaderboard = users  # 这里 leaderboard 和 users 是同一个列表

# 后来又按名字排序
users.sort(key=lambda u: u["name"])
print(leaderboard)  # 顺序被覆盖了

因为leaderboard = users只是给同一个列表起了个别名,没有复制。users.sort()修改的是同一个列表,leaderboard看到的也是修改后的结果。

正确的做法是用sorted()生成新列表:

python 复制代码
users = [{"name": "Alice", "score": 85}, ...]

leaderboard = sorted(users, key=lambda u: u["score"], reverse=True)
by_name = sorted(users, key=lambda u: u["name"])

print(users)        # 原顺序不变
print(leaderboard)  # 按积分排序
print(by_name)      # 按名字排序

这样三个列表互不影响。

排序的稳定性

Python的排序是稳定排序。意思是:如果两个元素比较相等,它们在排序后的相对顺序和排序前一致。

ini 复制代码
data = [
    {"name": "Alice", "score": 85},
    {"name": "Bob", "score": 92},
    {"name": "Charlie", "score": 85},
]

data.sort(key=lambda x: x["score"])
print(data)

输出:

css 复制代码
[    {"name": "Alice", "score": 85},    {"name": "Charlie", "score": 85},    {"name": "Bob", "score": 92},]

Alice和Charlie的积分相同,都是85。排序后Alice仍在Charlie前面,因为她们在原列表里就是这个顺序。

这个特性在多重排序时很有用。比如先按积分排序,积分相同的再按名字排序:

ini 复制代码
data.sort(key=lambda x: x["name"])          # 先按名字排
data.sort(key=lambda x: x["score"], reverse=True)  # 再按积分排

因为排序是稳定的,第二次排序时,积分相同的元素会保持第一次排序后的名字顺序。这实现了"积分优先,积分相同按名字"的效果。

如果用sorted(),要注意每次都要接收新列表:

ini 复制代码
by_name = sorted(data, key=lambda x: x["name"])
by_score = sorted(by_name, key=lambda x: x["score"], reverse=True)

key 参数:排序的核心

sort()和sorted()都支持key参数。key是一个函数,接收一个元素,返回一个用于比较的值。

scss 复制代码
words = ["banana", "apple", "cherry"]
words.sort(key=len)
print(words)  # ['apple', 'banana', 'cherry']

按长度排序,长度相同的保持原顺序(稳定排序)。

也可以按多个条件排序,用元组作为key:

ini 复制代码
data = [("Alice", 85), ("Bob", 92), ("Charlie", 85)]
data.sort(key=lambda x: (x[1], x[0]))
print(data)

先按积分升序,积分相同按名字升序。

如果积分要降序,名字要升序,可以这样:

ini 复制代码
data.sort(key=lambda x: (-x[1], x[0]))

用负数实现降序,是一个常见技巧。但只适用于数值。如果要对字符串降序,得用reverse=True,但那会把所有字段都降序。

reverse 参数

sort()和sorted()都支持reverse参数:

ini 复制代码
nums = [3, 1, 4, 1, 5]
nums.sort(reverse=True)
print(nums)  # [5, 4, 3, 1, 1]

result = sorted([3, 1, 4, 1, 5], reverse=True)
print(result)  # [5, 4, 3, 1, 1]

reverse=True表示降序。默认是升序。

注意:reverse=True和先升序再反转结果不完全一样。因为稳定排序,先升序再反转会打乱相等元素的相对顺序。而reverse=True在排序时就处理了,相等元素保持原顺序。

性能差异

sort()是原地排序,不需要额外分配列表空间。sorted()需要创建一个新列表,占用额外内存。

在时间上,两者差不多,都是O(n log n)。但sorted()多了一次复制操作,所以稍微慢一点点。

python 复制代码
import timeit

lst = list(range(1000, 0, -1))

t1 = timeit.timeit(lambda: sorted(lst), number=10000)
t2 = timeit.timeit(lambda: lst.copy().sort(), number=10000)

print(f"sorted(): {t1:.4f}")
print(f"sort(): {t2:.4f}")

在我的机器上,sorted()大约0.6秒,sort()大约0.5秒。差距不大,但确实存在。

如果你不需要保留原列表,用sort()更省内存。如果你需要保留原列表,用sorted()。

什么时候用 sort(),什么时候用 sorted()?

判断标准很简单:

需要保留原列表的顺序 :用sorted()。

不需要保留原顺序,只想把原列表排好 :用sort()。

对非列表的可迭代对象排序 :只能用sorted()。

在函数参数里排序 :用sorted(),因为sort()会修改传入的列表,可能影响调用者。

链式调用 :用sorted(),因为它返回新列表,可以继续调用其他方法:

ini 复制代码
result = sorted(data, key=lambda x: x["score"])[:10]

而sort()返回None,不能链式调用。

一个常见的误解:sorted() 比 sort() 慢很多

有些人觉得sorted()创建新列表,所以比sort()慢很多。其实不然。

sorted()确实多了一次复制,但复制是O(n),排序是O(n log n)。当n很大时,排序的时间占主导,复制的开销可以忽略。

python 复制代码
import timeit

lst = list(range(100000, 0, -1))

t1 = timeit.timeit(lambda: sorted(lst), number=100)
t2 = timeit.timeit(lambda: lst.copy().sort(), number=100)

print(f"sorted(): {t1:.4f}")
print(f"sort(): {t2:.4f}")

在我的机器上,sorted()大约2.1秒,sort()大约2.0秒。差距只有5%左右。

所以选择sort()还是sorted(),主要看语义需求,而不是性能。

排序不可变对象

如果你有一个元组列表,想按某个字段排序,只能用sorted():

python 复制代码
users = (
    ("Alice", 85),
    ("Bob", 92),
    ("Charlie", 78),
)

result = sorted(users, key=lambda u: u[1], reverse=True)
print(result)

元组本身不可变,不能原地排序。sorted()返回一个新的列表,里面是排好序的元组。

回到小郑的故事

小郑最终的代码是这样的:

python 复制代码
users = [
    {"name": "Alice", "score": 85},
    {"name": "Bob", "score": 92},
    {"name": "Charlie", "score": 78},
]

leaderboard = sorted(users, key=lambda u: u["score"], reverse=True)
by_name = sorted(users, key=lambda u: u["name"])

print("排行榜:", leaderboard)
print("按名字:", by_name)
print("原始数据:", users)  # 顺序不变

users始终保持原始顺序,两个排序结果互不影响。

他后来在团队文档里写了一条:"sort()原地改,返回None。sorted()不改原数据,返回新列表。需要保留原顺序就用sorted(),不需要就用sort()。别把sort()的返回值赋给变量。"

这个区分看起来简单,但真到写代码的时候,很多人会随手写nums = nums.sort(),然后在下一行打印时发现是None。或者用sort()排了序,然后发现原数据的顺序被改了,导致其他依赖原顺序的逻辑出错。

记住那个扑克牌的比喻:sort()是在原桌子上重新排,sorted()是另开一张桌子排。你要保留原桌子的顺序,就用sorted();你不在乎原顺序,只想把当前桌子整理好,就用sort()。

排序本身不难,难的是想清楚"这个排序会不会影响别的地方"。想清楚了,sort和sorted就不再是坑,而是两个各司其职的工具。

相关推荐
可乐ea3 小时前
2000 个智能体把 Agent 重写成 Rust:编排流水线拆解
开发语言·后端·rust·多智能体协作·ai编程助手·智能体编排·rust重写
用户72757483196503 小时前
02-变量、常量和字符串:len("Hello, 世界") 为什么不是 9?
后端
SamDeepThinking3 小时前
CyclicBarrier的底层实现
java·后端·面试
god_cvz3 小时前
第二章:6、对话记忆与引用标注
后端·ai编程
拖孩3 小时前
全程 AI 写的小程序,刚接上会员虚拟支付,上线第二天就有人真金白银买单了|双签名、定价、iOS 的坑我全踩了一遍
前端·后端·微信小程序
我是人✓3 小时前
SpringMVC异常处理与拦截器实战揭秘
java·开发语言·后端
仟里码3 小时前
九:goroutine(1.什么是协程)
后端
不是万能的豆学长3 小时前
排查Go服务40天后的内存泄漏问题
开发语言·后端·golang·go
阿狗童鞋3 小时前
SpringSecurity权限认证实战指南
java·后端·spring
韩振方3 小时前
Kubernetes Service 让会更换的 Pod 有稳定访问入口
后端