sort方法实现复杂排序
1.概述
python为我们提供了sort函数实现排序需求,sort函数除了基础的自然顺序排序,它还有一个强大的排序功能,通过key参数实现函数式排序可以满足各种排序需求。
2.sort函数排序
2.1.基础用法
在默认情况下,sort方法总是按照自然升序排列列表内的元素。例如,如果列表中的元素都是整数,那么它就按数值从小到大排列。
number = [93, 86, 11, 68, 70]
number.sort()
print(number)
运行上面的示例代码,查看排序结果
[11, 68, 70, 86, 93]
如果在sort函数中传入reverse参数就能从大到小排序
number = [93, 86, 11, 68, 70]
number.sort(reverse=True)
print(number)
运行上面的示例代码,查看排序结果
[93, 86, 70, 68, 11]
2.2.高级排序
凡是具备自然顺序的内置类型几乎都可以用sort方法排列,例如字符串、浮点数、整数等。但是,一般的对象又该如何排序呢?我们创建的自定义对象排序就需要用到高级排序方法了。
比如 我们定义了一个Tool类表示各种建筑工具,__repr__方法用于输出对象的属性
class Tool:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __repr__(self):
return f'Tool({self.name}, {self.weight})'
tools = [
Tool('beijing', 3),
Tool('shandong', 1.3),
Tool('changsha', 6.9),
Tool('nanyang', 2.9)
]
print(tools)
运行上面的代码,结果显示我们创建了一个tools列表,列表中的值就是Tool类对象属性
[Tool(beijing, 3), Tool(shandong, 1.3), Tool(changsha, 6.9), Tool(nanyang, 2.9)]
1.key参数是一个lambda表达式排序
- 这些排序标准通常是针对对象中的某个属性(attribute)。
- 我们可以把这样的排序逻辑定义成函数,然后将这个函数传给sort方法的key参数。
- key所表示的函数本身应该带有一个参数,这个参数指代列表中有待排序的对象,函数返回的应该是个可比较的值(也就是具备自然顺序的值),以便sort方法以该值为标准给这些对象排序。
下面用lambda关键字定义这样的一个函数,把它传给sort方法的key参数,让我们能够按照name的字母顺序排列这些Tool对象
# x代表待排序的对象这里代指tools对象,x.name表示按照tools对象的name属性排序
tools.sort(key=lambda x: x.name)
print(tools)
运行上面的代码,输出结果按照name属性字母顺序排序
[Tool(beijing, 3), Tool(changsha, 6.9), Tool(nanyang, 2.9), Tool(shandong, 1.3)]
如果想改用另一项标准(比如weight)来排序,那只需要再定义一个lambda函数并将其传给sort方法的key参数就可以了。
tools.sort(key=lambda x: x.weight, reverse=True)
print(tools)
运行上面的代码,输出结果按照weight属性数字倒序排序
[Tool(changsha, 6.9), Tool(beijing, 3), Tool(nanyang, 2.9), Tool(shandong, 1.3)]
在编写传给key参数的lambda函数时,可以像刚才那样返回对象的某个属性,如果对象是序列、元组或字典,那么还可以返回其中的某个元素。其实,只要是有效的表达式,都可以充当lambda函数的返回值。
对于字符串这样的基本类型,我们可能需要通过key函数先对它的内容做一些变换,并根据变换之后的结果来排序。
例如,下面的这个places列表中存放着表示地点的字符串,如果想在排列的时候忽略大小写,那我们可以先用lower方法把待排序的字符串处理一下(因为对于字符串来说,自然顺序指的就是它们在词典里的顺序,而词典中大写字母在小写字母之前)
places = ['home', 'work', 'New York', 'Paris']
# x.lower() 将字符串转为小写在排序
places.sort(key=lambda x: x.lower())
print(places)
运行上面的代码,输出结果显示按照小写字母顺序排序
['home', 'New York', 'Paris', 'work']
2.多个指标排序
有时我们可能需要用多个标准来排序。例如,下面的列表里有一些电动工具,我们想以weight(重量)为首要指标来排序,在重量相同的情况下,再按name(名称)排序。这应该怎么实现呢?
- 在Python语言里,最简单的方案是利用元组(tuple)类型实现。
- 元组是一种不可变的序列,能够存放任意的Python值。两个元组之间是可以比较的,因为这种类型本身已经定义了自然顺序,也就是说,sort方法所要求的特殊方法(例如__lt__方法),它都已经定义好了。
- 元组在实现这些特殊方法时会依次比较每个位置的那两个对应元素,直到能够确定大小为止。
利用元组的这项特性,我们可以用工具的weight与name构造一个元组。下面就定义这样一个lambda函数,让它返回这种元组,把首要指标(也就是weight)写在前面,把次要指标(也就是name)写在后面。
power_tools = [
Tool('drill', 4),
Tool('circular saw', 5),
Tool('jackhammer', 40),
Tool('sander', 4),
]
power_tools.sort(key=lambda x: (x.weight, x.name))
print(power_tools)
运行上面的代码,输出结果显示按照重量从小到大排序,如果重量相等则按照name排序。
[Tool(drill, 4), Tool(sander, 4), Tool(circular saw, 5), Tool(jackhammer, 40)]
3.定制多个指标排序方式
上面的做法有个缺点,就是key函数所构造的这个元组只能按同一个排序方向来对比它所表示的各项指标(要是升序,就都得是升序;要是降序,就都得是降序),所以不太好实现weight按降序排而name按升序排的效果。sort方法可以指定reverse参数,这个参数会同时影响元组中的各项指标。
如果其中一项指标是数字,那么可以在实现key函数时,利用一元减操作符让两个指标按照不同的方向排序。也就是说,key函数在返回这个元组时,可以单独对这项指标取相反数,并保持其他指标不变,这就相当于让排序算法单独在这项指标上采用逆序。下面就演示怎样按照重量从大到小、名称从小到大的顺序排列(这次,'sander’会排在’drill’的后面)
# weight属性类型是数字可以使用-号让它排序反转
power_tools.sort(key=lambda x: (-x.weight, x.name))
print(power_tools)
运行上面的代码,输出结果显示按照重量从大到小排序,如果重量相等则name按照从小到大排序
[Tool(jackhammer, 40), Tool(circular saw, 5), Tool(drill, 4), Tool(sander, 4)]
但是,这个技巧并不适合所有的类型。例如,若想在指定reverse=True的前提下得到相同的排序结果,那我们可以试着对name运用一元减操作符,试试能不能做出重量从大到小、名称从小到大排的效果。
power_tools.sort(key=lambda x: (x.weight, -x.name))
print(power_tools)
运行上面的代码,可以看到,str类型不支持一元减操作符
TypeError: bad operand type for unary -: 'str'
在这种情况下,我们应该考虑sort方法的一项特征,那就是这个方法是个稳定的排序算法。
我们可以在同一个列表上多次调用sort方法,每次指定不同的排序指标。下面我们就利用这项特征实现刚才想要达成的那种效果。把首要指标(也就是重量)降序放在第二轮,把次要指标(也就是名称)升序放在第一轮。
# 先将列表按照name从小达到排序
power_tools.sort(key=lambda x: x.name)
# 将排好序的列表在按照weight倒序排序
power_tools.sort(key=lambda x: x.weight, reverse=True)
print(power_tools)
运行上面的代码,可以看到重量按照从大到小排序,name按照从小到大排序
[Tool(jackhammer, 40), Tool(circular saw, 5), Tool(drill, 4), Tool(sander, 4)]
为什么这样可以得到正确的结果呢?我们分开来看。
先看第一轮排序,也就是按照名称升序排列
power_tools.sort(key=lambda x: x.name)
print(power_tools)
# 输出结果
[Tool(circular saw, 5), Tool(drill, 4), Tool(jackhammer, 40), Tool(sander, 4)]
然后执行第二轮,也就是按照重量降序排列。这时,由于’sander’与’drill’所对应的两个Tool对象重量相同,key函数会判定这两个对象相等。于是,在sort方法的排序结果中,它们之间的先后次序就跟第一轮结束时的次序相同。所以,我们在实现了按重量降序排列的同时,保留了重量相同的对象在上一轮排序结束时的相对次序,而上一轮是按照名称升序排列的。
power_tools.sort(key=lambda x: x.weight, reverse=True)
print(power_tools)
# 输出结果
[Tool(jackhammer, 40), Tool(circular saw, 5), Tool(drill, 4), Tool(sander, 4)]
无论有多少项排序指标都可以按照这种思路来实现,而且每项指标可以分别按照各自的方向来排,不用全都是升序或全都是降序。只需要倒着写即可,也就是把最主要的那项排序指标放在最后一轮处理。在上面的例子中,首要指标是重量降序,次要指标是名称升序,所以先按名称升序排列,然后按重量降序排列。
要点总结
- 列表的sort方法可以根据自然顺序给其中的字符串、整数、元组等内置类型的元素进行排序。
- 普通对象如果通过特殊方法定义了自然顺序,那么也可以用sort方法来排列,但这样的对象并不多见。
- 可以把辅助函数传给sort方法的key参数,让sort根据这个函数所返回的值来排列元素顺序,而不是根据元素本身来排列。
- 如果排序时要依据的指标有很多项,可以把它们放在一个元组中,让key函数返回这样的元组。
- 对于支持一元减操作符的类型来说,可以单独给这项指标取反,让排序算法在这项指标上按照相反的方向处理。
- 如果这些指标不支持一元减操作符,可以多次调用sort方法,并在每次调用时分别指定key函数与reverse参数。最次要的指标放在第一轮处理,然后逐步处理更为重要的指标,首要指标放在最后一轮处理。