Python排序详解:sorted() vs list.sort()

在Python中排序数据时,我们通常会使用sorted()list.sort()这两个函数。它们存在一些关键的区别,适用于不同的场景。本文将通过详细的示例阐述两者的差异,以及何时使用哪个函数更加合适。

list.sort() 的特点

list.sort()是列表对象的一个方法,作用是就地(in-place)对列表进行排序。也就是说,它直接修改了原列表,没有返回值:

1
2
3
nums = [3, 1, 2]
nums.sort()
print(nums) # [1, 2, 3]

list.sort()支持一些可选参数来自定义排序:

  • reverse=True 倒序排序
  • key 根据自定义函数结果排序

例如:

1
2
3
4
5
6
7
nums = [3,-2,1]

nums.sort(reverse=True)
print(nums) # [3, 1, -2] 倒序

nums.sort(key=abs)
print(nums) # [1,-2, 3] 根据绝对值排序

list.sort()只能用于列表,不能用于其他可迭代对象。它通过下标排序,稳定性不确定,相同元素的相对顺序可能会改变。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from dataclasses import dataclass

@dataclass
class User:
name: str
age: int

users = [
User('John', 25),
User('Alice', 25),
User('Bob', 24)
]

# 原始顺序为 John, Alice, Bob

users.sort(key=lambda x: x.age)

print([u.name for u in users])

# 结果可能为:
# ['Bob', 'John', 'Alice']
# 或者
# ['Bob', 'Alice', 'John']

按照年龄排序,John和Alice的年龄相同,但是这是两个不同的人,list.sort()可能不会保留原顺序。

list.sort()主要适用于:就地排序列表,不需要保留原顺序。

sorted() 的特点

sorted()是一个全局的内置函数,可以对任意可迭代对象进行排序,返回一个新列表。原列表不变:

1
2
3
4
5
nums = [3, 1, 2]  
new_nums = sorted(nums)

print(new_nums) # [1, 2, 3]
print(nums) # [3, 1, 2] 没变

sorted()也可以接受 reversekey 自定义排序:

1
2
3
4
5
new_nums = sorted(nums, reverse=True) # 倒序
print(new_nums) # [3, 2, 1]

new_nums = sorted(nums, key=str) # 按字符串排序
print(new_nums) # [1, 2, 3]

sorted() 是一个稳定排序,相同元素的相对顺序会保持不变。

主要适用于:

  • 需要保留原列表顺序的场景
  • 对不可变对象如 tuple 进行排序
  • 链式调用多个排序操作
  • 需要自定义排序逻辑的场景

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pprint import pprint

users = [{'name':'John', 'age':20},
{'name':'John', 'age':15},
{'name':'Lisa', 'age':25}]

# 根据年龄排序
by_age = sorted(users, key=lambda u: u['age'])

# 再根据名字排序
by_age_name = sorted(by_age, key=lambda u: u['name'])

pprint(by_age_name)

# ------------- Result -----------
# [{'age': 15, 'name': 'John'},
# {'age': 20, 'name': 'John'},
# {'age': 25, 'name': 'Lisa'}]

使用 attrgetter 和 itemgetter 代替 lambda 函数

key 参数中传入 lambda 函可以实现自定义排序,但是使用 operator 模块的 attrgetteritemgetter 可以使代码更简洁:

itemgetter 其实就是重载了[]下标操作符,可以用于获取序列的下标和字典的键:

1
2
3
4
5
6
7
8
9
from pprint import pprint
from operator import itemgetter

users = [{'name':'John', 'age':20},
{'name':'John', 'age':15},
{'name':'Lisa', 'age':25}]

by_age_name = sorted(users, key=itemgetter( 'name','age'))
pprint(by_age_name)

attrgetter 可以用于获取对象属性:

1
2
3
4
5
6
7
8
9
10
11
12
from operator import attrgetter
from pprint import pprint

users = [
User('John', 25),
User('Alice', 25),
User('Bob', 24)
]

# 根据年龄属性排序
by_age = sorted(users, key=attrgetter('age'))
pprint(by_age)

它们提供了一种更优雅和高效的提取对象属性和字典键的方式。

attrgetter 和 itemgetter 性能如何?

我们可以写个简单的测试来比较一下在sorted()中使用lambda和attrgetter的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import timeit

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

people = [
Person("Diana", 40),
Person("Frank", 45),
Person("Alice", 30),
Person("Bob", 25),
Person("Charlie", 35),
Person("Eve", 20)
]

# Using lambda function to sort by 'age'
lambda_time = timeit.timeit(
stmt="sorted(people, key=lambda person: person.age)",
setup="from __main__ import people",
number=100000
)

# Using attrgetter to sort by 'age'
attrgetter_time = timeit.timeit(
stmt="sorted(people, key=attrgetter('age'))",
setup="from __main__ import people; from operator import attrgetter",
number=100000
)

print(f"Using lambda: {lambda_time:.6f} seconds")
print(f"Using attrgetter: {attrgetter_time:.6f} seconds")

# Using lambda: 0.055244 seconds
# Using attrgetter: 0.047022 seconds

可以看到,使用 attrgetter 的性能比 lambda 函数更好,因为底层是C语言实现的。

总结

  • list.sort() 用于就地排序列表,更简单直接
  • sorted() 更通用更灵活,可以保留原顺序
  • attrgetteritemgetter 可以简化 key 函数,同时提高性能

根据实际需求选择合适的排序函数,可以让代码更高效和可读!