诀窍是让您的 key 函数返回一个元组,该元组在第一个索引中返回具有保证可比类型,在后续索引中返回不同类型。
虽然与 Python 2 的功能不是 100% 相同,但对于“数字在前面,其他所有内容都按类型名称进行比较”的特定情况,您可以使用相当有效的 key 函数来做到这一点:
>>> from numbers import Number
>>> seq = ['Z', 3, 'Y', 1, 'X', 2.5, False, (1, 2), [2, 3], None]
>>> sorted(seq, key=lambda x: (x is not None, '' if isinstance(x, Number) else type(x).__name__, x))
[None, False, 1, 2.5, 3, [2, 3], 'X', 'Y', 'Z', (1, 2)]
这里的key 函数使key 的第一个元素成为简单的bool,强制None 在其他所有内容之前排序(Py2 做了同样的事情),然后使用键的第二部分的空字符串,其他所有内容都使用它们的类型名称(也像 Py2)。一旦您通过了前两个索引,剩下的就是相同的类型,并且应该可以比较。
这里的主要缺陷是类似的非数字类型,如set 和frozenset 不会相互比较,它们将仅按类型名排序(使用异常的自定义键类可以处理这个问题)。
它也不会处理递归情况;如果序列包含[2, 3] 和['a', 'b'],它将有一个TypeError 将2 与'a' 进行比较,但只有一个可笑的关键类才能处理这个问题。
如果这不是问题,那么它运行起来很便宜并且相对简单。
与涉及定义__lt__ 以执行比较的自定义类的解决方案不同,这种方法具有生成内置键的优势,在排序期间与Python 级代码的最少执行进行有效比较。
时间安排:
# Multiply out the sequence so log n factor in n log n work counts for something
>>> seq = ['Z', 3, 'Y', 1, 'X', 2.5, False, (1, 2), [2, 3], None] * 100
# Verify equivalence
>>> sorted(seq, key=Py2Key) == sorted(seq, key=lambda x: (x is not None, '' if isinstance(x, Number) else type(x).__name__, x))
True
# Timings in seconds for the fastest time (of 3 trials) to run the sort 1000 times:
>>> import timeit
# Py2Key class
>>> min(timeit.repeat('sorted(seq, key=Py2Key)', 'from __main__ import seq, Py2Key', number=1000))
5.251885865057375
>>> min(timeit.repeat('sorted(seq, key=lambda x: (x is not None, "" if isinstance(x, Number) else type(x).__name__, x))', 'from __main__ import seq, Number', number=1000))
1.9556877178131344
基本上,避免动态 Python 级 __lt__ 的开销将运行时间减少了 60% 以上。这似乎不是算法上的改进(seq 100 倍的运行时间比相同),只是减少了固定开销,但这是一个不平凡的减少。