基础理论
以下内容适用于内置的sorted
函数和列表的.sort
方法。
通常,用于排序的key
函数可以简单地生成一个元组,其中每个元素对应于我们想要用于排序的“键”之一。这些元组将按字典顺序排序, 因此这会产生所需的结果-元素根据第一个键结果排序,平局由第二个键等解决。
同时,用于排序的reverse
关键字参数可以指定应以相反的顺序进行排序。它相当于正常排序,然后翻转结果,但更有效率。
然而,这个reverse
设置适用于整个排序。它不允许先按一个键升序排列,然后按另一个键降序排列,反之亦然。
示例设置
可以对包含任何类型对象的列表进行排序,而不仅仅是嵌套的列表/元组;并且可以编写处理这些对象的键函数,以任何方式处理这些对象 - 例如,根据特定属性的值对类的实例进行排序。为了清晰起见(即为了使用属性名称),我将设置一个简单的namedtuple
并演示对实例列表进行排序的技巧。
from collections import namedtuple
datum = namedtuple('datum', 'id age first last')
data = [
datum(1, 23, 'Foo', 'Bar'),
datum(2, 42, 'Baz', 'Quux'),
]
特殊情况:按两个数字键排序
为了模拟反向排序,只需取一个数值的负值。因此:
data.sort(key=lambda d: (d.id, -d.age))
data.sort(key=lambda d: (-d.id, d.age), reverse=True)
特殊情况:按最多一个非数字键排序
如果只有一个非数字键,则选择是否使用reverse
可以避免仅数字键可以以这种方式取反的问题:
data.sort(key=lambda d: (d.first, -d.id))
data.sort(key=lambda d: (-d.age, d.last), reverse=True)
使用包装类来取反值
一个更通用的方法是创建一个包装类negated
,语义为negated(x) < negated(y)
当且仅当x >= y
。这是在black panda的回答中采用的方法。因此:
class negated:
def __init__(self, obj):
self.obj = obj
def __eq__(self, other):
return other.obj == self.obj
def __lt__(self, other):
return other.obj < self.obj
data.sort(lambda d: (negated(d.last), d.first))
更为复杂:适应函数而非值
假设存在某个现有的关键函数 my_key
,我们想要按其结果降序排序,然后按其他某个关键字升序排序。我们可以像这样调整它,而不是重写 my_key
:
def negated_result(func):
return lambda x: negated(func(x))
data.sort(lambda d: (negated_result(my_key)(d), d.id))
negated_result
接受一个函数并返回一个函数,因此它也可以用作装饰器。
如果一切都失败了:按键重复排序
由于 Python 的内置排序是稳定的保证,我们可以简单地按第二个键排序,然后按第一个键排序:
data.sort(lambda d: d.id)
data.sort(my_key, reverse=True)
这个想法是在应用主排序的同时保留子排序。但是要记住以相反的顺序执行这个操作有点棘手,因此可能需要一个包装函数。例如:
from operator import attrgetter
from enum import Flag
class SortOrder(Flag):
DESCENDING = True
ASCENDING = False
def multi_sort(a_list, *specs):
'''Sort by multiple, optionally reversed keys.
specs -> a sequence of (func, bool) tuples.
Each tuple specifies a key func to use for sorting,
and whether or not to reverse the sort.'''
for key, reverse in reversed(specs):
a_list.sort(key=key, reverse=bool(reverse))
multi_sort(
data,
(my_key, SortOrder.DESCENDING),
(attrgetter('id'), SortOrder.ASCENDING)
)