Python - 使用__lt__运算符在pyQt中对数字进行人性化排序

2 投票
4 回答
4165 浏览
提问于 2025-04-16 13:24

我有一些数据行,想把它们展示成下面这个样子:

1
1a
1a2
2
3
9
9.9
10
10a
11
100
100ab
ab
aB
AB

因为我在使用pyQt,并且代码是在一个TreeWidgetItem里面,所以我现在要解决的代码是:

def __lt__(self, otherItem):
    column = self.treeWidget().sortColumn()

    #return self.text(column).toLower() < otherItem.text(column).toLower()

    orig = str(self.text(column).toLower()).rjust(20, "0")
    other = str(otherItem.text(column).toLower()).rjust(20, "0")
    return orig < other

4 个回答

0

这里有一个函数,它可以处理一个包含字母和数字的字符串,并返回一个元组,这个元组可以按照“自然”的方式进行排序。

def naturalkey(key, convert=int):
    if not key:
        return ()
    keys = []
    start = 0
    extra = ""
    in_num = key[0].isdigit()
    for i, char in enumerate(key):
        if start < i:
            if in_num:
                try:
                    last_num = convert(key[start:i])
                except:
                    in_num = False
                    if i > 2 and key[i-2] == ".":
                        extra = "."
                    keys.append(last_num)
                    start = i-1
            if not in_num:  # this is NOT equivalent to `else`!
                if char.isdigit():
                    keys.append(extra + key[start:i])
                    in_num = True
                    start = i
                    extra = ""
                    last_num = convert(char)
    keys.append(last_num if in_num else (extra + key[start:]))
    return tuple(keys)

这个函数的基本思路是,当它遇到数字时,会收集后面的字符,并不断尝试把这些字符转换成数字,直到无法再转换为止(也就是说,遇到错误)。默认情况下,它会把字符序列转换成整数,但你可以传入 convert=float 让它也能处理小数点。(不幸的是,它不支持科学计数法,比如 '1e3',因为它会先尝试解析 '1e',这会出错。虽然可以特别处理这种情况,但看起来在你的使用场景中并不必要。)

这个函数返回的元组包含了字符串和数字,顺序和它们在原字符串中出现的顺序一致,数字会被转换成你指定的类型。例如:

naturalkey("foobar2000.exe")
>>> ("foobar", 2000, ".exe")

这个元组可以用作排序字符串列表的关键字:

my_list.sort(key=lambda i: naturalkey(i, float))

或者你可以用它来实现一个比较函数:

def __lt__(self, other):
    return naturalkey(self.value, float) < naturalkey(other.value, float)

更好的做法是,在对象的 __init__() 方法中生成自然排序的关键字,把它存储在实例中,然后让你的比较函数使用这个存储的值。如果这个关键字的来源值是可变的,你可以写一个属性,当底层值更新时更新这个关键字。

2

使用samplebiasswapcase想法,以及Ned Batchelder的人性化排序代码,你可以这样做:

import re
def human_keys(astr):
    '''
    alist.sort(key=human_keys) sorts in human order
    '''
    keys=[]
    for elt in re.split('(\d+)', astr):
        elt=elt.swapcase()
        try: elt=int(elt)
        except ValueError: pass
        keys.append(elt)
    return keys

x='''
    1
    1a
    1a2
    2
    3
    9
    9.9
    9.10
    9a2
    10
    10a
    11
    100
    100ab
    ab
    aB
    AB
    '''.split()

print(x)
assert x == sorted(x,key=human_keys)

你可以在__lt__中应用human_keys,像这样:

def __lt__(self, otherItem):
    column = self.treeWidget().sortColumn()
    orig = str(self.text(column).toLower()).rjust(20, "0")
    other = str(otherItem.text(column).toLower()).rjust(20, "0")
    return human_keys(orig) < human_keys(other)
6

这可能对你有帮助。你可以编辑正则表达式,让它匹配你感兴趣的数字模式。我的设置会把包含.的数字字段当作浮点数来处理。使用swapcase()函数可以把字母的大小写反转,这样'A'就会排在'a'后面。

更新: 精炼内容:

import re

def _human_key(key):
    parts = re.split('(\d*\.\d+|\d+)', key)
    return tuple((e.swapcase() if i % 2 == 0 else float(e))
            for i, e in enumerate(parts))

nums = ['9', 'aB', '1a2', '11', 'ab', '10', '2', '100ab', 'AB', '10a',
    '1', '1a', '100', '9.9', '3']
nums.sort(key=_human_key)

print '\n'.join(nums)

输出结果:

1
1a
1a2
2
3
9
9.9
10
10a
11
100
100ab
ab
aB
AB

更新: (对评论的回应)如果你有一个类Foo,并想用_human_key排序方案来实现__lt__,只需返回_human_key(k1) < _human_key(k2)的结果即可;

class Foo(object):

    def __init__(self, key):
        self.key = key

    def __lt__(self, obj):
        return _human_key(self.key) < _human_key(obj.key)

>>> Foo('ab') < Foo('AB')
True
>>> Foo('AB') < Foo('AB')
False

所以在你的情况下,你可以这样做:

def __lt__(self, other):
    column = self.treeWidget().sortColumn()
    k1 = self.text(column)
    k2 = other.text(column)
    return _human_key(k1) < _human_key(k2)

其他比较运算符(__eq____gt__等)也可以用同样的方法来实现。

撰写回答