关于排序:Python在列表中排序不同的类型

Python sort different types in list

我需要使用python 3对列表进行排序。可能有stringsintegersfloatstuples等。

我目前正在尝试使用这样的key参数正确使用sort函数。

1
2
3
4
5
6
7
8
9
10
11
data.sort(key=gen_key)

...

def gen_key(self, value):
        if is_number(value):
            return str(value)

        if isinstance(value, str):
            return value
    return '___' + type(value).__name__

但问题是现在数字将被自然排序。当我想要排序数字和浮点数时,仍然像数字和浮点数一样,而不是将它们作为字符串进行线程化。

这种行为是由return str(value)部分引起的。但我不能返回与字符串不同的类型,因为这将引发一个异常,因为对于Python3,字符串不会像在Python2中那样用数字排序。例外情况如下

1
unordarable types: int() < str()

有什么建议吗?


诀窍是让您的key函数返回一个在第一个索引中有保证的可比较类型的元组,并在随后的索引中返回不同类型的元组。

虽然与python 2所做的并不完全相同,但对于特定的"前面的数字,其他所有与typename比较的内容",您可以使用一个相当有效的key函数来实现这一点:

1
2
3
4
>>> 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)。一旦你通过了前两个指数,剩下的是相同的类型,应该比较一下。

这里的主要缺陷是,类似于setfrozenset这样的非数字类型无法相互比较,它们只能按类型名排序(使用异常的自定义密钥类可以处理这一问题)。

它也不会处理递归的情况;如果序列包含[2, 3]['a', 'b'],它将有一个TypeError,比较2'a',但是除了一个荒谬的涉及的密钥类之外,没有任何东西可以处理这个问题。

如果这不是一个问题,那么运行起来就便宜,而且相对简单。

与使用定义为执行比较的__lt__的自定义类的解决方案不同,此方法具有生成内置密钥的优势,与排序期间最小程度地执行Python级别的代码相比,这些内置密钥是有效的。

时间安排:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 # 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%以上。这似乎不是算法上的改进(seq100倍的时间比相同),只是减少了固定开销,但这是非常重要的减少。


最干净的方法是将对象用作排序键,该对象在其比较方法中包含所需的排序行为。python排序所需的唯一比较方法是__lt__(),因此这相当简单。

例如,这里有一个类,它大致实现了python 2排序启发式(在可比较的对象组中按值排序)。当然,你可以执行你喜欢的任何其他规则。由于排序将为列表中的每个项目创建这些对象中的一个,我通过使用__slots__和插入所有类型字符串尽可能地减小每个对象的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sys import intern

class Py2Key:

    __slots__ = ("value","typestr")

    def __init__(self, value):
        self.value   = value
        self.typestr = intern(type(value).__name__)

    def __lt__(self, other):
        try:
            return self.value < other.value
        except TypeError:
            return self.typestr < other.typestr

使用:

1
2
3
seq = ["Z", 3,"Y", 1,"X", 2.5, False]
sorted(seq, key=Py2Key)
>>> [False, 1, 2.5, 3, 'X', 'Y', 'Z']

不幸的是,在python 3中实现python 2的排序行为将比python2慢,而且内存密集,特别是因为我们利用了异常处理。这在您的应用程序中是否可以接受取决于您自己。