Python 中的排序工具:sort 与 sorted 详解
转载请注明出处:https://blog.youkuaiyun.com/jpch89/article/details/84324272
文章目录
0. 参考资料
1. 排序基础
Python
中提供了两种排序工具:
list.sort()
方法,它会修改原列表。sorted()
内置函数,从可迭代对象生成一个新的排序后的列表。
最基本的升序排序很简单:
- 调用
sorted()
函数,返回一个排序过的列表,不修改原列表。
>>> old_list = [1, 4, 5, 3, 2, 8, 7]
>>> new_list = sorted(old_list)
>>> new_list
[1, 2, 3, 4, 5, 7, 8]
>>> old_list # 不改变原列表
[1, 4, 5, 3, 2, 8, 7]
- 也可以用
list.sort()
方法,它会修改原列表,返回None
。通常它没有sorted()
方便,但是如果不需要原列表的话,它更高效些。
>>> old_list
[1, 4, 5, 3, 2, 8, 7]
>>> old_list.sort()
>>> old_list
[1, 2, 3, 4, 5, 7, 8]
注意:
由于list.sort()
方法返回的是None
,所以如果错误地写出old_list = old_list.sort()
这样的代码,就会丢失old_list
的所有数据。
- 还有一个区别是,
list.sort()
方法只有列表可以使用,而sorted()
函数可以作用于任何可迭代对象。
>>> sorted({1: 'D', 4: 'B', 2: 'C', 3: 'E', 5: 'A'})
[1, 2, 3, 4, 5]
2. key 参数
list.sort()
和 sorted()
都有一个 key
参数,它接收一个函数 function
,比较之前会对每个元素调用一次。
比如下面是忽略大小写的字符串比较。
- 使用
sorted()
函数:
>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
- 使用
list.str()
方法:
>>> strings = "This is a test string from Andrew".split()
>>> strings.sort(key=str.lower)
>>> print(strings)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
key
参数的值应该是一个函数,这个函数接收一个参数,返回一个用于排序的键。
key
函数对每一个元素调用一次。
def key_func(x):
print('我被调用了')
return x.lower()
strings = "This is a test string from Andrew".split()
strings.sort(key=key_func)
print(strings)
"""
我被调用了
我被调用了
我被调用了
我被调用了
我被调用了
我被调用了
我被调用了
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
"""
比较常见的一个应用场景是取对象的某个索引作为键。
student_tuples = [
('john', 'A', 15),
('jane', 'B', 12),
('dave', 'B', 10)]
# sort by age
print(sorted(student_tuples, key=lambda student: student[2]))
"""
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
"""
也可以使用对象的某个属性作为键。
class Student:
def __init__(self, name, grade, age):
self.name = name
self.grade = grade
self.age = age
def __repr__(self):
return repr((self.name, self.grade, self.age))
student_objects = [
Student('john', 'A', 15),
Student('jane', 'B', 12),
Student('dave', 'B', 10)]
# sort by age
print(sorted(student_objects, key=lambda student: student.age))
"""
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
"""
3. operator 模块中的函数
上述的应用场景太普遍了,所以 Python
提供了 operator
模块中的三个函数(严格来讲是三个类),可以使用它们代替手写匿名函数:itemgetter()
、attrgetter()
和 methodcaller()
。
itemgetter(item, ...)
接收一个或者多个参数,返回一个可调用对象,该可调用对象根据传入的参数从其操作数中获取索引,多个索引会以元组的形式返回。
from operator import itemgetter
alist = [0, 1, 2, 3, 4]
adict = {'姓': '王', '名': '二小'}
func = itemgetter(1)
print(func(alist))
"""
1
"""
func = itemgetter(-2)
print(func(alist))
"""
3
"""
func = itemgetter(0, 2, 4)
print(func(alist))
"""
(0, 2, 4)
"""
func = itemgetter('姓', '名')
print(func(adict))
"""
('王', '二小')
"""
attrgetter(attr, ...)
接收一个或者多个表示属性名的字符串,返回一个可调用对象,该可调用对象根据传入的字符串从其操作数中获取属性,多个属性会以元组的形式返回。
from operator import attrgetter
class Name:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
def __repr__(self):
return f'{self.first_name}·{self.last_name}'
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
name = Name('小龙', '李')
person = Person(name, 20)
func = attrgetter('name', 'age')
print(func(person))
"""
(小龙·李, 20)
"""
func = attrgetter('name.first_name', 'name.last_name')
print(func(person))
"""
('小龙', '李')
"""
methodcaller(name, ...)
接收字符串形式的函数名及其他参数,返回一个可调用对象,该可调用对象会根据传入的函数名及其他参数对其操作数进行调用。
from operator import methodcaller
class Person:
def greet(name='', text=''):
print(f'{name}向你问好!{text}')
func = methodcaller('greet')
func(Person)
"""
向你问好!
"""
func = methodcaller('greet', '牛魔王')
func(Person)
"""
牛魔王向你问好!
"""
func = methodcaller('greet', '红孩儿', text='吃了吗?')
func(Person)
"""
红孩儿向你问好!吃了吗?
"""
operator
模块内的这三个函数在比较时的用法举例如下:
from operator import itemgetter, attrgetter
print(sorted(student_tuples, key=itemgetter(2)))
"""
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
"""
print(sorted(student_objects, key=attrgetter('age')))
"""
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
"""
使用 operator
模块中的函数可以进行多级排序,比如先按照 grade
再按照 age
排序。
print(sorted(student_tuples, key=itemgetter(1, 2)))
"""
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
"""
print(sorted(student_objects, key=attrgetter('grade', 'age')))
"""
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
"""
4. 升序与降序
list.sort()
方法和 sorted()
函数都接收一个 reverse
参数,默认为 False
,如果设置为 True
,则会降序排序。
按照年龄降序,对学生进行排序:
print(sorted(student_tuples, key=itemgetter(2), reverse=True))
"""
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
"""
print(sorted(student_objects, key=attrgetter('age'), reverse=True))
"""
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
"""
5. 排序稳定性和复杂排序
使用这两个工具排序是稳定的。
这意味着如果多条记录拥有同样的键,它们的原始顺序保持不变,如下图所示。
下面的示例代码中,蓝1在蓝2前面,红1在红2前面,都保持了原顺序不变。
data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
print(sorted(data, key=itemgetter(0)))
"""
[('blue', 1), ('blue', 2), ('red', 1), ('red', 2)]
"""
稳定性可以让我们进行复杂排序。
- 需求:对学生数据按成绩降序排列,按年龄升序排列。
- 解决:可以先对年龄进行升序排序,再对成绩进行降序排列。
# sort on secondary key
s = sorted(student_objects, key=attrgetter('age'))
# now sort on primary key, descending
s = sorted(s, key=attrgetter('grade'), reverse=True)
print(s)
"""
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
"""
Python
中使用了Timsort
算法,可以高效处理多次排序。
https://en.wikipedia.org/wiki/Timsort
6. 使用老式的 DSU 方法排序
DSU
是 Decorate-Sort-Undecorate
的缩写,这种方法总共有三个步骤:
- 用新的值来装饰原始列表,该值在排序中会被用到
- 对装饰之后的列表进行排序
- 去除装饰,创建只含有原始值的已排序列表
比如对学生数据按照成绩 grade
进行排序:
# D
decorated = [(student.grade, i, student) for i, student in enumerate(student_objects)]
print(decorated)
"""
[('A', 0, ('john', 'A', 15)), ('B', 1, ('jane', 'B', 12)), ('B', 2, ('dave', 'B', 10))]
"""
# S
decorated.sort()
print(decorated)
"""
[('A', 0, ('john', 'A', 15)), ('B', 1, ('jane', 'B', 12)), ('B', 2, ('dave', 'B', 10))]
"""
# U
undecorated = [student for grade, i, student in decorated]
print(undecorated)
"""
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
"""
元组会先比较第一项,如果相同,比较第二项。
之所以使用 enumerate
为原列表加上索引,是因为这样可以保证排序的稳定性。
如果对稳定性没有要求,可以不用 enumerate
。
还有一个优点是原列表的元素不一定非要是可比较的,比如原列表包含复数,但加入索引,最多比较两项就能确定顺序。
DSU
排序也叫作 Schwartzian transform,是 Randal L. Schwartz
在 Perl
程序员之间推广起来的。
7. 使用老式的 cmp 参数排序
Python 2.4
以前没有 sorted()
内置函数,list.sort()
也不接受 key
参数。所有的 Python 2.x
版本通过 cmp
参数处理用户自定义的比较函数(在 Python 3.0
中,cmp
参数被完全移除)。
Python 2.x
中可选的 cmp
参数接收一个函数。这个函数接收两个参数,当返回值等于 0
的时候表示两个参数相等,当返回值大于 0
的时候表示第一个参数大于第二个参数,当返回值小于 0
的时候表示第一个参数小于第二个参数。举例如下:
>>> def numeric_compare(x, y):
... return x - y
>>> sorted([5, 2, 4, 1, 3], cmp=numeric_compare) # doctest: +SKIP
[1, 2, 3, 4, 5]
反向排序:
>>> def reverse_numeric(x, y):
... return y - x
>>> sorted([5, 2, 4, 1, 3], cmp=reverse_numeric) # doctest: +SKIP
[5, 4, 3, 2, 1]
在进行 Python 2.x
到 3.x
的代码迁移时,可以使用下面的方式把 cmp
参数接收的函数转换为 key
参数接收的函数。
def cmp_to_key(mycmp):
'Convert a cmp= function into a key= function'
class K:
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K
cmp
参数接收的函数可以叫做比较函数comparison function
。
key
参数接收的函数可以叫做键函数key function
。
通过 cmp_to_key
将老式的比较函数包裹起来就可以转换成键函数。
>>> sorted([5, 2, 4, 1, 3], key=cmp_to_key(reverse_numeric))
[5, 4, 3, 2, 1]
在 Python 3.2
中,cmp_to_key
函数被添加到了标准库的 functools
模块中。
8. 杂项
- 如果要进行本地化的比较,使用
locale.strxfrm()
作为键函数,使用locale.strcoll()
作为比较函数。 reverse
参数具有排序稳定性,具有相同键的记录保持原始次序不变。这种效果可以通过两次调用内置函数reversed()
来模拟。
>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
>>> standard_way = sorted(data, key=itemgetter(0), reverse=True)
>>> double_reversed = list(reversed(sorted(reversed(data), key=itemgetter(0))))
>>> assert standard_way == double_reversed
>>> standard_way
[('red', 1), ('red', 2), ('blue', 1), ('blue', 2)]
- 排序过程中一定会用到
__lt__()
方法来比较两个对象,所以只要给一个类添加__lt__()
方法即可方便地定义排序规则。
>>> Student.__lt__ = lambda self, other: self.age < other.age
>>> sorted(student_objects)
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
- 键函数不一定直接依赖于被排序的对象。键函数同样可以访问外部资源。例如,学生成绩在一个字典中,它可以用来对一个独立存在的学生名字列表进行排序:
>>> students = ['dave', 'john', 'jane']
>>> newgrades = {'john': 'F', 'jane':'A', 'dave': 'C'}
>>> sorted(students, key=newgrades.__getitem__)
['jane', 'dave', 'john']
完成于 2019.05.27