Python 中的排序工具:sort 与 sorted 详解

本文详细介绍了Python中的排序工具sort和sorted,包括两者的基本用法、key参数、operator模块函数、升序降序排序、排序稳定性以及老式DSU方法和cmp参数的使用。示例代码展示了如何进行多级排序和自定义比较函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 方法排序

DSUDecorate-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. SchwartzPerl 程序员之间推广起来的。


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.x3.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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值