聊聊python中排序:sort与sorted

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']

参考文章:

  1. Python 排序—sort与sorted学习
  2. Python3中排序函数sort()和sorted()
  3. python3 sorted 如何自定义排序标准
  4. python3 sort 去除了cmp怎么自定义排序(一个降序,一个升序)
  5. Python3自定义key函数排序
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章