Python3 排序指南

来自 Unsplash

问题引入

在 Leetcode 中遇到一道题目,题目中需要对一个元组列表排序,排序规则是先按照元组第一个元素大小排序,如果第一个元素相等,则按照第二个元素排序。

tuples = [(1, 4), (2, 3), (6, 10), (4, 8), (2, -3), (4, 5)]
# 将这个元组列表排序,先按元组第一个元素排序;如果第一个元素相等,按照第二个元素排序。

下面开始这篇关于 Python3 排序的文章。

注意:下文中的 Python 均代表 Python3。

看完这篇文章,你会知道下面的内容

  • Python 中怎样进行排序
  • 在 Python 中怎样使用 sorted() 和 sort()
  • 形参 reverse 和 key 的作用
  • 对于自定义类型列表的排序方法(比如,先按属性 A 排序,再按属性 B 排序)
  • 该问题的解法

文章的内容主要参考自这两篇文章。这两篇文章写的很好,一篇是中文的,另一篇是英文的。推荐大家看一下:

  1. How to Use sorted() and sort() in Python - realpython.com
  2. 排序指南-官方中文文档

用 sort() 和 sorted() 排序

在编程的过程中,经常会遇到这样的需求:把一个整数数组升序排序。
我们可以自己实现「快速排序」和「堆排序」等等常用的排序算法来实现这个需求。
除了自己实现之外,也可以通过调用语言自带的库来解决。比如 Python 中就提供了两个函数来排序: sorted() 和 sort()。

nums = [3,10,6,19,-3,4]
# 使用sorted
new_nums = sorted(nums)
print(nums)  # [3, 10, 6, 19, -3, 4]
print(new_nums) # [-3, 3, 4, 6, 10, 19]
# 使用sort
nums.sort()  
print(nums) # [-3, 3, 4, 6, 10, 19]

sort() 和 sorted() 有什么区别

从上的代码中不难看出一个区别:
sort() 是直接对原列表进行排序的,而 sorted() 会创建一个新的列表来存储排序结果,不会修改原数组。
除此之外,sort() 只能被列表调用,而 sorted() 可以被任何「可迭代对象」调用。也就是说如果要对元组进行排序,那么只能用 sorted 了。

nums = (3, 10, 6, 19, -3, 4)
nums.sort()
"""
Traceback (most recent call last):
  File "demo.py", line 21, in <module>
    nums.sort()
AttributeError: 'tuple' object has no attribute 'sort'
"""
new_nums = sorted(nums)
print(new_nums)  # [-3, 3, 4, 6, 10, 19]
print(nums)  # (3, 10, 6, 19, -3, 4)

细心的读者应该会发现一个有趣的地方,经过 sorted 处理后的元组变成了列表。这是为什么呢?再对字典排一下序试试看。

maps = {1: 'D', 4: 'E', 2: 'B', 3: 'B', 5: 'A'}
print(sorted(maps))  # [1, 2, 3, 4, 5]

果然,最后的结果还是列表。

查阅文档,发现 sorted 会返回一个新的已排序列表

那么问题又来了,对字典排序,最后的结果怎么变成了列表呢?难道不应该是字典吗?

其实,sorted 是默认对字典的键排序的,而不会对值排序。

如果我想得到按照键排序的字典怎么办呢?少啰嗦,看代码。

maps = {1: 'D', 4: 'E', 2: 'B', 3: 'B', 5: 'A'}
sorted_maps ={key: maps[key] for key in sorted(maps)}
print(sorted_maps) # {1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'}

总结一下:

  • sort 只能被列表调用(类似于 list 类中的一个方法),而 sorted 可以别任意可迭代对象调用。
  • sort 是直接对原列表排序,而 sorted 不会改变原列表(或者其他可迭代对象),而是返回一个新的已排序列表。记住是列表

用 reverse 进行排序

如果想要降序排序呢?这时候 inverse 就派上用场了。
sort 和 sorted 函数都可以接受布尔类型的 reverse 形参,默认值是 False,表示按照关键字大小升序排序。当 reverse 是 True 时,按照关键字大小降序排列。所以,想要降序排列,只需将 reverse 指定为 True 即可。

nums = [3, 10, 6, 19, -3, 4]
new_nums = sorted(nums,reverse=True)
print(new_nums)  # [19, 10, 6, 4, 3, -3]
nums.sort(reverse=True)
print(nums)  # [19, 10, 6, 4, 3, -3]

用 key 进行排序

读到这里,我们会发现上面的的 sort 和 sorted 函数太死板了,不方便扩展。而 key 就是来解决这个问题的。

Python 中的 sort 和 sorted 都可以接收 key 这个形参,用来指定比较之前对每个列表元素调用的函数,所以 key 必须是可调用的(callable)并且只能接收单个参数的函数对象。

那怎么用呢?借用一下文档的例子,不区分字符串大小写的排序。

sorted("This is a test string from Andrew".split(), key=str.lower)  
# ['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']
sorted("This is a test string from Andrew".split())
# ['Andrew', 'This', 'a', 'from', 'is', 'string', 'test']

需要注意的是:传到 key 的函数必须只能接收一个参数,并且能够处理列表(或者其它可迭代对象)元素的函数对象。

借用 Real Python 的例子。

def add(x, y):
	return x + y

values_to_add = [1, 2, 3]
sorted(values_to_add, key=add)
"""
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: add() missing 1 required positional argument: 'y'
只能传一个参数到 key 对应的函数中
"""

values_to_cast = ['1', '2', '3', 'four']
sorted(values_to_cast, key=int)
"""
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'four'
字符串 ‘four’ 不能调用 int
"""

对自定义类型的列表排序

假设我们现在有一个学生类 Student,拥有 name 、grade和age 三个属性。

class Student:
    def __init__(self, name, grade, age):
        self.name = name
        self.grade = grade
        self.age = age
    # 用于格式化打印,类似于 Java 中的 toString()
    def __repr__(self):
        return repr((self.name, self.grade, self.age))

现在有一个列表,其中的元素是 Student 的一个实例。

student_objects = [
    Student('john', 'A', 15),
    Student('jane', 'B', 12),
    Student('dave', 'B', 10),
]

首先第一个需求:按照年龄对这个列表降序排列。将 lambda ele: ele.age 传入到 key 中,就解决了这个问题。关于 lambda 表达式,建议看一下这篇文章

student_objects.sort(reverse=True, key=lambda ele: ele.age)
print(student_objects)  # [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

# 关于 Python lambda 函数的语法可以看一下这篇文章
# https://dbader.org/blog/python-lambda-functions

第二个需求:先按照成绩升序排列,如果成绩相同,按照名字升序排列。

这就有点难了,上面刚刚提到过,key 对应的函数只能接受一个参数,而现在是要接收两个参数,这可怎么办呢?我的第一反应是这样的。

student_objects.sort(key= lambda ele: ele.grade or ele.name)

看了上面的内容,我们知道 key 接受的是一个能执行的函数,现在我们传入的是一个布尔类型的值,这肯定不对。查阅文档,知道了 operator 模块。少啰嗦,先看代码。

from operator import attrgetter
print(sorted(student_objects, key=attrgetter('grade', 'age')))
# [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

首先导入了 operator 模块。然后对 student_objects 调用 sorted 函数,传入的 key 是 atttgetter('grade', 'age')。这代表什么意思呢?

operator. attrgetter( attr )

Return a callable object that fetches attr from its operand. If more than one attribute is requested, returns a tuple of attributes. The attribute names can also contain dots.

返回一个可从操作数中获取 attr 的可调用对象。 如果请求了一个以上的属性,则返回一个属性元组。 属性名称还可包含点号。

– 摘录自文档

形参 key 「用来指定比较之前对每个列表元素调用的函数」,所以上面的代码其实就是在对 student_objects 排序之前,首先对每个元素 item 调用 attrgetter(‘grade’, ‘age’),返回属性元组 (item.grade, item.age)。借助这个特性实现了需求二。

当然,因为是列表,上面的代码也可以用 sort。如果是元组的话,那只能用 sorted。

对元组列表排序

tuples = [(1, 4), (2, 3), (6, 10), (4, 8), (2, -3), (4, 5)]
# 将这个元组列表排序,先按元组第一个元素排序;如果第一个元素相等,按照第二个元素排序。

刚才我们说到 attrgetter(‘grade’, ‘age’),返回属性元组 (item.grade, item.age)。然后 Python 似乎默认就依次按照元组元素大小来排序了。所以简单的调用 sorted 应该就能解决了。少啰嗦,看代码。

print(sorted(tuples))
# [(1, 4), (2, -3), (2, 3), (4, 5), (4, 8), (6, 10)]

果然,解决了,竟然如此简单。

从这个需求出发,稍微改动一下需求,假设我们只有两个列表,列表元素是数字。现在的需求是返回一个已排序的元组列表,列表元素是元组。其中第一个元素来自第一个列表,第二个元素来自第二个列表。要求依次按照元组元素的顺序升序排列,就是说如果第一个元素相等,按照第二元素大小排序,以此类推。现在怎么做呢?

其实改动之后的需求就多了一个将两个列表打包成一个元组列表的过程。提到打包自然要调用 zip。下面是代码。

nums1 = [1, 2, 6, 4, 2, 4]
nums2 = [4, 3, 10, 8, -3, 5]
tuples = sorted(zip(nums1, nums2))
# [(1, 4), (2, -3), (2, 3), (4, 5), (4, 8), (6, 10)]

继续改,现在不按照元组元素顺序来排序了,而是先按照元组第二个元素排序,如果第二个元素相等再按照第一个元素排序,也就是「顺序颠倒」。

现在要怎么办呢?

第一个很容易想到的思路是,把每个元组反转,然后再调用 sorted 排序,最后再反转回去。

有没有再简单点的方法呢?

我们可以借用 operator 模块的 itemgetter 函数。少啰嗦,先看代码。

from operator import itemgetter  # 切记要导入 itemgetter 模块
tuples.sort(key=itemgetter(1,0))
print(tuples)  # [(2, -3), (2, 3), (1, 4), (4, 5), (4, 8), (6, 10)

有了 attrgetter 的经验,这里的 itemgetter 就容易理解一些了。调用 sort 传入 itemgetter(1, 0) 到形参 key , 表示在比较前对每个元素(这里是元组)调用了 itemgetter(1, 0) 方法。

operator.itemgetter (item)

返回一个使用操作数的 __getitem__() 方法从操作数中获取 item 的可调用对象。 如果指定了多个条目,则返回一个查找值的元组。

– 同样摘录自官方文档

所以最终会返回一个元组,原来元组下标是 1 的元素在前, 下标是 0 的元素在后。说白了就是一个反转操作。最终实现了需求。

文章到这里差不多介绍完了,下面做个总结:

  • Python 提供的排序函数有两个: sort 和 sorted ;
  • sort 只能被列表调用,并且直接对原列表进行排序,元素返回值是 None ;
  • sorted 可以被任何可迭代对象调用,不直接对原对象排序,而是返回一个已排序的新的列表
  • sort 和 sorted 都可以接收 key 和 reverse 这两个形参。其中 key 是一个可以被执行的且只有一个参数的函数;reverse 是一个布尔类型的参数,默认是 False,表示升序排列,反之,True 代表按照降序排列。
  • 切记:sort 直接对原列表进行排序,而 sorted 不会修改原对象。我们需要针对不同场景,灵活使用这两个函数。

那么,如果要对一个字符串按照 ASCII 码的顺序升序排列,我们要用哪个函数呢?这个问题留给你们思考。

全文完。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值