诀窍是让你的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)]
The key
这里的函数使第一个元素key
一个简单的bool
, 强迫None
在其他所有内容之前进行排序(Py2 做了同样的事情),然后首先使用键的第二部分的空字符串对所有数字类型进行排序,其他所有内容都使用它们的类型名称(也像 Py2 一样)。一旦你超越了前两个索引,剩下的就是相同的类型,并且应该可以很好地进行比较。
这里的主要缺陷是类似的非数字类型set
and frozenset
不会相互比较,它们将仅按类型名称排序(使用异常的自定义键类可以处理此问题)。
它也不会处理递归情况;如果序列包含[2, 3]
and ['a', 'b']
,它将有一个TypeError
比较2
with 'a'
,但是只有一个可笑的关键类才能处理这个问题。
如果这不是问题,那么运行起来很便宜并且相对简单。
与涉及自定义类的解决方案不同__lt__
定义来执行比较,此方法的优点是生成内置键,可以在排序期间以最少的 Python 级代码执行进行有效比较。
Timings:
# 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% 以上。这似乎不是算法的改进(aseq
100 倍长具有相同的运行时间比),只是固定开销的减少,但这是一个不平凡的减少。