0 前言
有时候我们需要自定义排序的规则,但是又不想自己再写一遍排序函数,所以库中的排序函数一般都支持我们自定排序规则,然后以函数的形式传递给排序函数,就完成了自定义排序的需求。之前一直用C++,知道C++中的排序函数sort是可以实现这种需求的。现在转到python3突然发现python3提供的sort函数没有了cmp这个参数(python2是有的),但是幸运的是,我们还可以通过其他的方法实现这种自定义排序规则的需求。
1 python2中的sort和sorted
1.1 sort()函数
sort函数的定义如下:
sort(self, cmp=None, key=None, reverse=False)
- self:表示list自身
- cmp:自定的比较函数
- key:指定元素在比较之前要调用的函数,并且这个函数接受一个参数,返回一个作为排序依据的key。
- reverse:当为False时,表示增序排列,为True时为降序排列。
sort函数仅仅对列表对象进行排序,会改变list元素自身的顺序,没有返回值也可以认为返回值为None。调用的形式为list.sort()
python2中使用sort函数自定义排序函数来进行排序:
a = [('ZhangSan',24),('Lisi', 22) , ('WangWu', 20)]
#python中升序排序的比较函数的逻辑
def cmp(a, b):
if a < b:
return -1
elif a > b:
return 1
else:
return 0
print a
a.sort(cmp=cmp, key=lambda x: x[1])
print a
输出结果:
[('ZhangSan', 24), ('Lisi', 22), ('WangWu', 20)]
[('WangWu', 20), ('Lisi', 22), ('ZhangSan', 24)]
1.2 sorted()函数
sorted()函数的定义:
sorted(iterable, cmp=None, key=None, reverse=False)
- iterable表示可迭代的类型
- 其他的参数和sort()函数一致
sorted函数与sort函数相比有两点主要的不同:
- sorted函数不会改变原来的可迭代的集合,而是会返回一个新的排序好的列表(list)
- sort函数只能用于list,sorted函数可以用于任意可迭代的类型
python2中使用sorted函数自定义排序函数来进行排序:
a = [('ZhangSan',24),('Lisi', 22) , ('WangWu', 20)]
#python中升序排序的比较函数的逻辑
def cmp(a, b):
if a < b:
return -1
elif a > b:
return 1
else:
return 0
print a
b = sorted(a, cmp, key=lambda x: x[1])
print a
print b
2 python3中的sort和sorted
2.1 在python3中使用cmp函数
在python3中sort函数和sorted函数都有所变化,他们的cmp参数都被去掉了。没有了cmp函数怎么进行自定义排序呢?其实python提供了另一种方法。就是使用functools.cmp_to_key函数将比较函数转换为key的参数。
import functools
a = [('ZhangSan',24),('Lisi', 22) , ('WangWu', 20), ('WangWu', 19)]
#python中升序排序的比较函数的逻辑
def cmp(a, b):
if a < b:
return -1
elif a > b:
return 1
else:
return 0
print (a)
a.sort(key=functools.cmp_to_key(cmp))
print (a)
[('ZhangSan', 24), ('Lisi', 22), ('WangWu', 20), ('WangWu', 19)]
[('Lisi', 22), ('WangWu', 19), ('WangWu', 20), ('ZhangSan', 24)]
可以看到,对于元组,默认按照第一个元素排序,第一个元素相同的话会比较第二个元素,依次往后比较。cmp函数需要两个参数,functools模板中的functools.cmp_to_key()就可以把原来有两个参数的cmp转化成了符合key参数要求的一个参数的函数。
cmp_to_key
函数的源码如下:
def cmp_to_key(mycmp):
"""Convert a cmp= function into a key= function"""
class K(object):
__slots__ = ['obj']
def __init__(self, obj):
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
__hash__ = None
return K
返回的是一个类,在 sort 内部,类接收一个参数构造一个实例,然后实例通过重载的方法来进行比较。
sorted函数同样支持上面的用法。
2.2 python3中自定义排序
上面介绍的是怎样在python3中使用cmp函数来自定义排序,cmp函数有两个参数,是python2的产物,在python中提倡使用key function,key function接收一个参数。我们完全可以自定义的排序规则定义为key function,而不是定义为cmp函数再使用cmp_to_key转换成key function。
必须再次明确key_function的作用:指定元素在比较之前要调用的函数,并且这个函数接受一个参数,返回一个作为排序依据的key。
下面是几个例子来帮助,我们适应使用key_function这种方式来自定义排序规则。
例子1:
my_alphabet = ['a', 'b', 'c']
def custom_key(word):
numbers = []
for letter in word:
numbers.append(my_alphabet.index(letter))
return numbers
# python中的整数列表能够比较大小
# custom_key('cbaba')==[2, 1, 0, 1, 0]
x=['cbaba', 'ababa', 'bbaa']
x.sort(key=custom_key)
print(x)
['ababa', 'bbaa', 'cbaba']
例子2:
students = [('john', 'A', 15), ('jane', 'B', 12), ('dave','B', 10)]
ns = sorted(students,key=lambda x: x[2]) #按照年龄来排序
print(ns)
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
例子3:使用对象的属性作为key值
>>> 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),
... ]
>>> sorted(student_objects, key=lambda student: student.age) # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
例子4:使用多个多个关键字进行排序
>>> list = [('d',3),('a',5),('d',1),('c',2),('d',2)]
>>> print (sorted(list, key = lambda x:(x[0],x[1])))
[('a', 5), ('c', 2), ('d', 1), ('d', 2), ('d', 3)]
例子5:
默认sorted([True, False])==[False, True] (False=0 < True=1)
一个字符串排序,排序规则:小写<大写<奇数<偶数
#元组内(e1, e2, e3)的优先级排列为: e1 > e2 > e3
s = 'asdf234GDSdsf23'
ns = sorted(s, key=lambda x: (x.isdigit(),x.isdigit() and int(x) % 2 == 0,x.isupper(),x)
print(ns)
#input: 'asdf234GDSdsf23'
#output: ['a', 'd', 'd', 'f', 'f', 's', 's', 'D', 'G', 'S', '3', '3', '2', '2', '4']
- x.isdigit()的作用是把数字放在后边(True),字母放在前面(False).
- x.isdigit() and int(x) % 2 == 0的作用是保证数字中奇数在前(False),偶数在后(True)。
- x.isupper()的作用是在前面基础上,保证字母小写(False)在前大写在后(True).
- 最后的x表示在前面基础上,对所有类别数字或字母排序。
小结:可以看到key_function非常灵活,我们可以定义各种各样的排序规则。其实python2中同样具有sort和sorted函数也是有key参数的,同样可以使用key function定义排序规则。
3 operator.itemgetter或者operator.attrgetter
首先介绍一下:operator模块。operator模块封装了python的一系列操作符对应的函数,某些情况下我们需要为函数传递一个操作符对应的函数,而不能直接传递一个操作符
这里仅仅介绍operator下面三种操作符在自定义排序中的应用。
- operator.itemgetter() ----- 通过下标获取属性
- operator.attrgetter() ----- 通过参数获取属性
- operator.methodcaller() -----通过函数名获取函数,python 2.5 被引入,下文详细介绍
from operator import itemgetter, attrgetter
student_tuples = [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10),]
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),]
print(sorted(student_tuples, key=itemgetter(2)))
print(sorted(student_objects, key=attrgetter('age')))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
这个操作模块也允许多层次的进行排序,例如可以先排序 “成绩grand” 再排序 “年龄age”
print(sorted(student_tuples, key=itemgetter(1,2)))
print(sorted(student_objects, key=attrgetter('grade', 'age')))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
itemgetter和attrgetter比较好理解,methodcaller其实不太好理解,关于methodcaller的详细介绍参考博客:python中的operator模块
下面看一个例子:
from operator import methodcaller
messages = ['critical!!!', 'hurry!', 'standby', 'immediate!!']
print(sorted(messages, key=methodcaller('count', '!')))
['standby', 'hurry!', 'immediate!!', 'critical!!!']
上面的例子完成的功能是,根据字符串中!的数量对字符串排序,!数量少的排在前面,多的排在后面。
为了帮助理解以上代码,补充一个知识,python的count()
函数:
描述: count() 方法用于统计字符串里某个字符出现的次数。可选参数为在字符串搜索的开始与结束位置。
语法: str.count(sub, start= 0,end=len(string))
参数:
- sub – 搜索的子字符串
- start – 字符串开始搜索的位置。默认为第一个字符,第一个字符索引值为0。
- end – 字符串中结束搜索的位置。字符中第一个字符的索引为 0。默认为字符串的最后一个位置。
4 排序的稳定性
从python2.2版本开始,python的排序是保障稳定性的,意思就是说,当复杂的排序中,对象有相同的key的时候,会保持原有的相对顺序不变:
data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
print(sorted(data, key=itemgetter(0)))
[('blue', 1), ('blue', 2), ('red', 1), ('red', 2)]
5 一个应用实例
具有串文件名['file1.txt', 'file2.txt', 'file10.txt']
需要按照文件名的数字后缀对文件名进行排序:
import re
re_digits = re.compile(r'(\d+)')
def emb_numbers(s):
pieces = re_digits.split(s)
pieces[1] = map(int, pieces[1])
return pieces
def sort_strings_with_emb_numbers(alist):
aux = [(emb_numbers(s), s) for s in alist]
aux.sort()
return [s for __, s in aux]
def sort_strings_with_emb_numbers2(alist):
return sorted(alist, key=emb_numbers)
filelist = 'file10.txt file2.txt file1.txt'.split()
print (filelist)
print ('--DSU排序')
print (sort_strings_with_emb_numbers(filelist))
print ('--内置DSU排序')
print (sort_strings_with_emb_numbers2(filelist) )
['file10.txt', 'file2.txt', 'file1.txt']
--DSU排序
['file1.txt', 'file10.txt', 'file2.txt']
--内置DSU排序
['file1.txt', 'file10.txt', 'file2.txt']
参考文章: