
第14条 用sort方法的key参数来表示复杂的排序逻辑
内置的列表类型提供了名叫sort
的方法,可以根据多项指标给list
实例中的元素排序。在默认情况下,sort
方法总是按照自然升序排列列表内的元素。例如,如果列表中的元素都是整数,那么它就按数值从小到大排列。

凡是具备自然顺序的内置类型几乎都可以用sort
方法排列,例如字符串、浮点数等。但是,一般的对象又该如何排序呢?比如,这里定义了一个Tool
类表示各种建筑工具,它带有__repr__
方法,因此我们能把这个类的实例打印成字符串(参见第75条)。

如果仅仅这样写,那么这个由该类的对象所构成的列表是没办法用sort
方法排序的,因为sort
方法发现,排序所需要的特殊方法并没有定义在Tool
类中。

如果某些类像整数(int
)那样具有自然顺序,那么可以定义一些特殊的方法(参见第73条),这样我们无须额外的参数就能直接在由这种类的实例所构成的列表上调用sort
方法来排序了。但更为常见的情况是,很多对象需要在不同的情况下按照不同的标准排序,此时定义自然排序实际上没有意义。
这些排序标准通常是针对对象中的某个属性(attribute)。我们可以把这样的排序逻辑定义成函数,然后将这个函数传给sort
方法的key
参数。key
所表示的函数本身应该带有一个参数,这个参数指代列表中有待排序的对象,函数返回的应该是个可比较的值(也就是具备自然顺序的值),以便sort
方法以该值为标准给这些对象排序。
下面用lambda
关键字定义这样的一个函数,把它传给sort
方法的key
参数,让我们能够按照name
的字母顺序排列这些Tool
对象。

如果想改用另一项标准(比如weight
)来排序,那只需要再定义一个lambda
函数并将其传给sort
方法的key
参数就可以了。

在编写传给key
参数的lambda
函数时,可以像刚才那样返回对象的某个属性,如果对象是序列、元组或字典,那么还可以返回其中的某个元素。其实,只要是有效的表达式,都可以充当lambda
函数的返回值。
对于字符串这样的基本类型,我们可能需要通过key
函数先对它的内容做一些变换,并根据变换之后的结果来排序。例如,下面的这个places
列表中存放着表示地点的字符串,如果想在排列的时候忽略大小写,那我们可以先用lower
方法把待排序的字符串处理一下(因为对于字符串来说,自然顺序指的就是它们在词典里的顺序,而词典中大写字母在小写字母之前)。

有时我们可能需要用多个标准来排序。例如,下面的列表里有一些电动工具,我们想以weight
(重量)为首要指标来排序,在重量相同的情况下,再按name
(名称)排序。这应该怎么实现呢?

在Python语言里,最简单的方案是利用元组(tuple
)类型实现。元组是一种不可变的序列,能够存放任意的Python值。两个元组之间是可以比较的,因为这种类型本身已经定义了自然顺序,也就是说,sort
方法所要求的特殊方法(例如__lt__
方法),它都已经定义好了。元组在实现这些特殊方法时会依次比较每个位置的那两个对应元素,直到能够确定大小为止。下面,我们看看其中一个工具比另一个工具重的情况,在这种情况下,只需要根据元组中的第一个元素(重量)就可以确定这两个元组的大小。

如果两个元组的首个元素相等,就比较第二个元素,如果仍然相等,就继续往下比较。下面演示两个重量相同但名称不相同的元组。

利用元组的这项特性,我们可以用工具的weight
与name
构造一个元组。下面就定义这样一个lambda
函数,让它返回这种元组,把首要指标(也就是weight
)写在前面,把次要指标(也就是name
)写在后面。

这种做法有个缺点,就是key
函数所构造的这个元组只能按同一个排序方向来对比它所表示的各项指标(要是升序,就都得是升序;要是降序,就都得是降序),所以不太好实现weight
按降序排而name
按升序排的效果。sort
方法可以指定reverse
参数,这个参数会同时影响元组中的各项指标(例如在下面的例子中,weight
与name
都会按照降序处理,所以'sander'
会出现在'drill'
的前面,而不是像刚才的例子那样出现在后面)。

如果其中一项指标是数字,那么可以在实现key
函数时,利用一元减操作符让两个指标按照不同的方向排序。也就是说,key
函数在返回这个元组时,可以单独对这项指标取相反数,并保持其他指标不变,这就相当于让排序算法单独在这项指标上采用逆序。下面就演示怎样按照重量从大到小、名称从小到大的顺序排列(这次,'sander'
会排在'drill'
的后面)。

但是,这个技巧并不适合所有的类型。例如,若想在指定reverse=True
的前提下得到相同的排序结果,那我们可以试着对name
运用一元减操作符,试试能不能做出重量从大到小、名称从小到大排的效果。

可以看到,str
类型不支持一元减操作符。在这种情况下,我们应该考虑sort
方法的一项特征,那就是这个方法是个稳定的排序算法。这意味着,如果key
函数认定两个值相等,那么这两个值在排序结果中的先后顺序会与它们在排序前的顺序一致。于是,我们可以在同一个列表上多次调用sort
方法,每次指定不同的排序指标。下面我们就利用这项特征实现刚才想要达成的那种效果。把首要指标(也就是重量)降序放在第二轮,把次要指标(也就是名称)升序放在第一轮。

为什么这样可以得到正确的结果呢?我们分开来看。先看第一轮排序,也就是按照名称升序排列:

然后执行第二轮,也就是按照重量降序排列。这时,由于'sander'
与'drill'
所对应的两个Tool
对象重量相同,key
函数会判定这两个对象相等。于是,在sort
方法的排序结果中,它们之间的先后次序就跟第一轮结束时的次序相同。所以,我们在实现了按重量降序排列的同时,保留了重量相同的对象在上一轮排序结束时的相对次序,而上一轮是按照名称升序排列的。

无论有多少项排序指标都可以按照这种思路来实现,而且每项指标可以分别按照各自的方向来排,不用全都是升序或全都是降序。只需要倒着写即可,也就是把最主要的那项排序指标放在最后一轮处理。在上面的例子中,首要指标是重量降序,次要指标是名称升序,所以先按名称升序排列,然后按重量降序排列。
尽管这两种思路都能实现同样的效果,但只调用一次sort
,还是要比多次调用sort
更为简单。所以,在实现多个指标按不同方向排序时,应该优先考虑让key
函数返回元组,并对元组中的相应指标取相反数。只有在万不得已的时候,才可以考虑多次调用sort
方法。
要点
- 列表的
sort
方法可以根据自然顺序给其中的字符串、整数、元组等内置类型的元素进行排序。 - 普通对象如果通过特殊方法定义了自然顺序,那么也可以用
sort
方法来排列,但这样的对象并不多见。 - 可以把辅助函数传给
sort
方法的key
参数,让sort
根据这个函数所返回的值来排列元素顺序,而不是根据元素本身来排列。 - 如果排序时要依据的指标有很多项,可以把它们放在一个元组中,让
key
函数返回这样的元组。对于支持一元减操作符的类型来说,可以单独给这项指标取反,让排序算法在这项指标上按照相反的方向处理。 - 如果这些指标不支持一元减操作符,可以多次调用
sort
方法,并在每次调用时分别指定key
函数与reverse
参数。最次要的指标放在第一轮处理,然后逐步处理更为重要的指标,首要指标放在最后一轮处理。