要在 Python 2.3 或更低版本中按键排序,可以使用cmp 参数。但有时key 样式排序更容易阅读;无论如何,它的工作量更少,因为 cmp 将被调用 O(n log n) 次,而 key 函数将仅被调用 O(n) 次。
考虑到这一点,这里有一种方法可以在更高版本的 Python 中重现 key 参数的行为。它使用 decorate-sort-undecorate 成语,也就是 Schwartzian Transform。这不会像复制副本那样节省空间,但是对于大型列表,它可能会更节省时间。我将其命名为sorted,因为它大致再现了2.4 中添加的sorted 函数;检查 python 版本并有条件地导入它,这样您就不会在较新版本中破坏内置的 sorted - 或者只是重命名它。
def sorted(seq, key=lambda x: None, reverse=False):
seq = [(key(x), i, x) for i, x in enumerate(seq)]
seq.sort()
if reverse:
seq.reverse()
return [x for k, i, x in seq]
请注意,enumerate 仅在您关心对具有相等键的不相等值进行稳定排序时才需要;它会减慢头发的功能。对您的数据进行了测试:
>>> key=lambda x: (x.count('YES'), x.count('MAYBE'), x.count('NO'))
>>> my_sorted(mylist, key=key, reverse=True)
[['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE'],
['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'],
['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO']]
您也可以考虑使用字典进行计数;这样,只需要一次通过。但是,count 已充分优化,至少在我的机器上,三遍仍然比一个 Python for 循环快。因此,仅当您需要计算大量值时才使用它。我将把它留在这里以供后代使用:
def my_key(inner_list):
counts = {'YES':0, 'MAYBE':0, 'NO':0}
for i in inner_list:
if i in counts:
counts[i] += 1
return (counts['YES'], counts['MAYBE'], counts['NO'])
我做了一些测试;为长篇大论道歉。以下内容仅供好奇者参考。
我的测试表明,在较小的列表中,装饰、排序、取消装饰已经比使用内置排序 + cmp 更快。在更大的列表中,差异变得更加显着。定义:
def key_count(x):
return (x.count('YES'), x.count('MAYBE'), x.count('NO'))
def key_dict(inner_list):
counts = {'YES':0, 'MAYBE':0, 'NO':0}
for i in inner_list:
if i in counts:
counts[i] += 1
return (counts['YES'], counts['MAYBE'], counts['NO'])
def decorate_sort(seq, key=lambda x: None, reverse=False):
seq = [(key(x), i, x) for i, x in enumerate(seq)]
seq.sort()
if reverse:
seq.reverse()
return [x for k, i, x in seq]
def builtin_sort(seq, key, reverse=False):
seq.sort(lambda p, q: cmp(key(p), key(q)))
if reverse:
seq.reverse()
测试:
>>> mylist = [
... ['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO'],
... ['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'],
... ['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE']
... ]
>>> %timeit decorate_sort(mylist, key=key_count, reverse=True)
100000 loops, best of 3: 5.03 us per loop
>>> %timeit builtin_sort(mylist, key=key_count, reverse=True)
100000 loops, best of 3: 5.28 us per loop
内置版本已经很慢了!不太通用的版本mylist.sort(lambda p, q: -cmp(key(p), key(q))) 是一个更好的短名单,因为enumerate 添加到decorate_sort;没有它,decorate_sort 更快(在我之前的测试中每个循环 4.28 us):
>>> %timeit mylist.sort(lambda p, q: -cmp(key_count(p), key_count(q)))
100000 loops, best of 3: 4.74 us per loop
虽然在这种情况下使用 key_dict 是错误的:
>>> %timeit decorate_sort(mylist, key=key_dict, reverse=True)
100000 loops, best of 3: 8.97 us per loop
>>> %timeit builtin_sort(mylist, key=key_dict, reverse=True)
100000 loops, best of 3: 11.4 us per loop
在更大的列表上进行测试,结果基本相同:
>>> import random
>>> mylist = [[random.choice(('YES', 'MAYBE', 'NO')) for _ in range(1000)]
for _ in range(100)]
>>> %timeit decorate_sort(mylist, key=key_count, reverse=True)
100 loops, best of 3: 6.93 ms per loop
>>> %timeit builtin_sort(mylist, key=key_count, reverse=True)
10 loops, best of 3: 34.5 ms per loop
不太通用的版本现在比decorate_sort慢。
>>> %timeit mylist.sort(lambda p, q: -cmp(key_count(p), key_count(q)))
100 loops, best of 3: 13.5 ms per loop
而key_dict 仍然较慢。 (但比builtin_sort 快!)
>>> %timeit decorate_sort(mylist, key=key_dict, reverse=True)
10 loops, best of 3: 20.4 ms per loop
>>> %timeit builtin_sort(mylist, key=key_dict, reverse=True)
10 loops, best of 3: 103 ms per loop
因此结果是,Schwartzian 变换提供了一个更快和更通用的解决方案——这是一种罕见而美妙的组合。