小郑那天写了一个排行榜功能,需要把用户按积分从高到低排序,然后展示前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就不再是坑,而是两个各司其职的工具。