sorted和sort的异同

不同之处

sort 只适用于list类型的数据,而sorted适用于一切python序列类型,包括可变类型(list, dict, set)和不可变类型(frozenset, str)等一切实现了__iter__方法的可迭代对象
sort是在原列表上进行排序(可通过id方法查看内存地址不变),返回值是None
sorted会创建一个新的列表存放排序结果,无论排序的是什么类型,最终返回值都是list
tip:所以这里要注意sort返回值是None,不能对返回值进行链式调用

# 两个函数返回值的比较
>>> print([5,4,3,2,1].sort())
None
>>> print(sorted([5,4,3,2,1]))
[1, 2, 3, 4, 5]
相同之处
sort(self, /, *, key=None, reverse=False)
sorted(iterable, /, *, key=None, reverse=False)

从上面可以看到二者的参数定义,都接受两个关键参数 keyreverse
key: 一个只有一个参数的方法,这个函数会被作用在每个元素上,该方法所产生的结果将作为排序比较的标准,默认值为元素本身的大小。
reverse:值为True时为降序排序,值为False时为升序排序,默认值为False。

key 关键字的用法

sort为list的内置函数,使用范围不大,但是如果序列是为list时使用sort的性能是优于sorted的,这里我们还是以sorted为基础,重点看一下key 关键字的不同应用场景,以下不涉及reverse参数,默认都是升序排序。

字典排序

先看一个例子

>>> sorted(dict(小明=18, 小红=17, 小白=20))
['小明', '小白', '小红']

上面给出的字典排序最终返回结果是有keys,这在实际应用场景中肯定是不行的。看以下代码,使用items()方法将字典元组化,key关键字接受一个匿名函数,这里x是每个元组,取元组下标为1代表是字典的value,按照value进行排序,排序完成后将使用字典工厂函数将元组恢复成字典。

>>> d = dict(小明=18, 小红=17, 小白=20)
>>> dict(sorted(d.items(), key=lambda x:x[1], reverse=True))
{'小白': 20, '小明': 18, '小红': 17}
列表排序

其实我们从上面可以看出,字典排序的本质就是将字典转为元组再进行排序的,那么对于元组或者列表的排序也是一样的道理,看一下列表排序

>>> person = [["小明", 18, 170], ["小红", 17, 160], ["小白", 20, 150]]

# 匿名函数单个参数
>>> sorted(person, key=lambda x:x[1])
[['小红', 17, 160], ['小明', 18, 170], ['小白', 20, 150]]

# 匿名函数多个参数,先根据x[2]排序,再根据x[1]排序
>>> sorted(person, key=lambda x:(x[2],x[1]))
[['小白', 20, 150], ['小红', 17, 160], ['小明', 18, 170]]
对象排序

如果是类实例根据类属性排序的话,同样的操作,匿名函数中的x表示拿到的每一个Person实例,根据object.attr拿到对象的属性进行比较

class Person:
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height

    def __repr__(self):
        return f'Person({self.name},{self.age},{self.height})'
   
>>> persons = [Person("小明",18,170), Person("小红",17,160), Person("小白",20,150)]

# 匿名函数接受单个参数
>>> sorted(persons, key=lambda x:x.age)
[Person(小红,17,160), Person(小明,18,170), Person(小白,20,150)]

# 匿名函数接受多个参数,先根据height排序,再根据age排序
>>> sorted(persons, key=lambda x: (x.height, x.age))
[Person(小白,20,150), Person(小红,17,160), Person(小明,18,170)]
字符串排序
>>> sorted('python java golang')
[' ', ' ', 'a', 'a', 'a', 'g', 'g', 'h', 'j', 'l', 'n', 'n', 'o', 'o', 'p', 't', 'v', 'y']

从上面的现象我们大概可以知道,如果是字符串排序的话字符串会先被按每一个字符切割:list(‘python java golang’),得到列表再进行排序,但我们实际想做的往往是做不同单词之间的排序,这时就要这样做了,看代码。

>>> sorted('python Java golang Python'.split())
['Java', 'Python', 'golang', 'python']

上个例子可以看出,字符串排序默认是按照ASCII升序排序,那么如果想忽略大小写按照字母表的顺序排序该如何传递key关键字呢,如果把在比较的时候把各个字符的大小写统一不就能实现了吗,这里使用lower(upper也行)在比较的时候将字符全部变为小写

>>> sorted('python Java golang Python'.split(), key=str.lower)
['golang', 'Java', 'python', 'Python']

还有一种情况,如果想按照字符串的长度排序呢,直接传递len方法就行

>>> sorted('python Java golang Python'.split(), key=len)
['Java', 'python', 'golang', 'Python']

itemgetter和attrgetter的简单分析

itemgetter和attrgetter两个类在官方文档示例中有提及被用来作为key属性,省去我们自己写匿名函数,并且其功能比较全面,我们来看一下这两个类的具体应用。

itemgetter
class itemgetter:

    def __init__(self, item, *items):
        if not items:
            self._items = (item,)
            def func(obj):
                return obj[item]
            self._call = func
        else:
            self._items = items = (item,) + items
            def func(obj):
                return tuple(obj[i] for i in items)
            self._call = func

    def __call__(self, obj):
        return self._call(obj)

从源码中可以看到这里定义了__call__方法,实现了类的函数式调用,sorted的key参数就是接受一个函数,这里就没毛病了,看该方法里面调用了self._call属性,该属性在__init__里面赋值。然后看__init__方法,在该方法里面又定义了一个方法func,最终将func赋给self._call方法,我们可以看得出来,如果__init__方法只有一个参数,根据obj中该参数位置的值进行排序;如果传递的参数为多个,则是返回一个元组,元组的元素为对象对应位置的各个值,依次按照这个顺序排序。看以下栗子

>>> from operator import itemgetter
>>> person= [['小明', 18, 170], ['小红', 17, 160], ['小白', 20, 150]]
>>> sorted(person, key=itemgetter(1))
[['小红', 17, 160], ['小明', 18, 170], ['小白', 20, 150]]

>>> sorted(person, key=itemgetter(2, 1))
[['小白', 20, 150], ['小红', 17, 160], ['小明', 18, 170]]

在第一个示例中只传递了1,那么就会根据每个元素index=1位置的值进行排序;
第二个示例中传递了2和1,那么就先根据index=2位置的值进行排序,再根据index=1位置的值进行排序

attrgetter
class attrgetter:

    def __init__(self, attr, *attrs):
        if not attrs:
            if not isinstance(attr, str):
                raise TypeError('attribute name must be a string')
            self._attrs = (attr,)
            names = attr.split('.')
            def func(obj):
                for name in names:
                    obj = getattr(obj, name)
                return obj
            self._call = func
        else:
            self._attrs = (attr,) + attrs
            getters = tuple(map(attrgetter, self._attrs))
            def func(obj):
                return tuple(getter(obj) for getter in getters)
            self._call = func

    def __call__(self, obj):
        return self._call(obj)

attrgetter和itemgetter类似,都是可以接受一个或多个参数作为排序的依据,attrgetter类接受的参数是对象的属性字符串,不同的是attrgetter 的 __init__方法在接收多个参数的情况下使用了递归,将多个参数的情况降解为一个参数的情况,看到当参数只有一个的情况,返回该对象所对应属性的值,然后根据这些值作为排序标准。举个栗子

>>> from operator import attrgetter
>>> sorted(persons, key=attrgetter('age'))
[Person(小红,17,160), Person(小明,18,170), Person(小白,20,150)]

>>> sorted(persons, key=attrgetter('height', 'age'))
[Person(小白,20,150), Person(小红,17,160), Person(小明,18,170)]

第一个示例根据age进行排序,这里相当于调用attrgetter(object)时返回的是obj.age;
第二个示例先根据height进行排序,再根据age进行排序,这里相当于调用attrgetter(object)时返回的是(obj.height, obj.age)。