Python自定義排序

python学习者0發表於2024-07-29

Python封裝了成熟的排序函式,我們只需要呼叫內部的sort函式,就可以完成排序。但是實際場景當中,排序的應用往往比較複雜,比如物件型別,當中有多個欄位,我們希望按照指定欄位排序,或者是希望按照多關鍵字排序,這個時候就不能簡單的函式呼叫來解決了。

1.字典排序

我們先來看下最常見的字典排序的場景,假設我們有一個字典的陣列,字典內有多個欄位。我們希望能夠根據字典當中的某一個欄位來進行排序,我們用實際資料來舉個例子:

kids = [
    {'name': 'xiaoming', 'score': 99, 'age': 12},
    {'name': 'xiaohong', 'score': 75, 'age': 13},
    {'name': 'xiaowang', 'score': 88, 'age': 15}
]

這裡的kids是一個dict型別的陣列,dict當中擁有name, score和age三個欄位。假設我們當下希望能夠按照score來排序,應該怎麼辦呢?

對於這個問題,解決的方案有很多,首先,我們可以使用上一篇文章當中提到的匿名函式來指定排序的。這裡的用法和上篇文章優先佇列的用法是一樣的,我們直接來看程式碼:

sorted(kids, key=lambda x: x['score'])

在匿名函式當中我們接收的x是kids當中的元素,也就是一個dict,所以我們想要指定我們希望的欄位,需要用dict訪問元素的方法,也就是用中括號來查詢對應欄位的值。

假如我們希望按照多關鍵字排序呢?

首先介紹一下多關鍵字排序,還是用上面的資料打比方。在上面的例子當中,各個kid的score都不一樣,所以排序的結果是確定的。但如果存在兩個人的score相等,我希望年齡小的排在前面,那麼應該怎麼辦呢?我們分析一下可以發現,原本是按照分數從小到大排序,但有可能會出現分數相等的情況。這個時候,我們希望能夠按照在分數相等的情況下來比較年齡,也就是說我們希望根據兩個關鍵字來排序,第一個關鍵字是分數,第二個關鍵字是年齡。

由於Python當中支援tuple和list型別的排序,也就是說我們可以直接比較[1, 3]和[1, 2]的大小關係,Python會自動一次比較兩個陣列當中的元素的大小。如果相等就自動往後比較,直到出現不等或者結束為止。

明白了這點,其實就很好辦了。我們只要在匿名函式當中稍稍修改,讓它返回的結果增加一個欄位即可。

sorted(kids, key=lambda x: (x['score'], x['age']))

2.itemgetter

除了匿名函式,Python也有自帶的庫可以解決這個問題。用法和匿名函式非常接近,使用起來稍稍容易一些。

它就是operator庫當中的itemgetter函式,我們直接來看程式碼:

from operator import itemgetter

sorted(kids, key=itemgetter('score'))

如果是多關鍵字也可以,傳入多個key即可:

sorted(kids, key=itemgetter('score', 'age'))

3.物件排序

我們接下來看一下物件的自定義排序,我們首先把上面的dict寫成物件:

class Kid:
    def __init__(self, name, score, age):
        self.name = name
        self.score = score
        self.age = age

    def __repr__(self):
        return 'Kid, name: {}, score: {}, age:{}'.format(self.name, self.score, self.age)

為了方便觀察列印結果,我們過載了__repr__方法,可以簡單地將它當做是Java當中的toString方法,這樣我們可以指定在print它的時候的輸出結果。

同樣,operator當中也提供了物件的排序因子函式,用法上和itemgetter一樣,只是名字不同。

from operator import attrgetter

kids = [Kid('xiaoming', 99, 12), Kid('xiaohong', 75, 13), Kid('xiaowang', 88, 15)]

sorted(kids, key=attrgetter('score'))

我們也可以使用匿名函式lambda來實現:

sorted(kids, key=lambda x: x.score)

4.自定義排序

到這裡還沒有結束,因為仍然存在一些問題解決不了。雖然我們實現了多關鍵字排序,但是還有一個問題解決不了,就是排序的順序問題。

我們可以在sorted函式的引數當中傳入reverse=True來控制是正序還是倒敘,但是如果我使用多關鍵字,想要按照某個關鍵字升序,某個關鍵字降序怎麼辦?舉個例子,比如說我們想要按照分數降序,年齡升序就沒辦法透過reverse來解決了,這就是當前解決不了的問題。

那應該怎麼辦呢?

這個時候就需要終極排序殺器上場了,也就是標題當中所說的自定義排序。也就是說我們自己實現一個定義元素大小的函式,然後讓sorted來呼叫我們這個函式來完成排序。這也是C++和Java等語言的用法。

自定義的函式並不難寫,我們隨手就來:

def cmp(kid1, kid2):
    return kid1.age < kid2.age if kid1.score == kid2.score else kid1.score > kid2.score

如果看不明白,也沒關係,我寫成完整版:

def cmp(kid1, kid2):
    if kid1.score == kid2.score:
        return kid1.age < kid2.age
    else:
        return kid1.score > kid2.score

寫完了之後,還沒有結束,這個函式是不能直接投入使用的,他和我們之前提到的lambda匿名函式是不一樣的。之前的匿名函式只是用來指定欄位的,所以我們不能直接將這個函式傳遞給key,還需要在外面包一層加工處理才可以。不過這一層處理函式Python也已經有現成的工具了,我們可以直接呼叫,它在functools裡,我們來看程式碼:

from functools import cmp_to_key

sorted(kids, key=cmp_to_key(cmp))

我們來看一下cmp_to_key函式里的原始碼:

def cmp_to_key(mycmp):
    """Convert a cmp= function into a key= function"""
    class K(object):
        __slots__ = ['obj']
        def __init__(self, obj):
            self.obj = obj
        def __lt__(self, other):
            return mycmp(self.obj, other.obj) < 0
        def __gt__(self, other):
            return mycmp(self.obj, other.obj) > 0
        def __eq__(self, other):
            return mycmp(self.obj, other.obj) == 0
        def __le__(self, other):
            return mycmp(self.obj, other.obj) <= 0
        def __ge__(self, other):
            return mycmp(self.obj, other.obj) >= 0
        __hash__ = None
    return K

我們可以看到,在函式內部,它其實定義了一個類,然後在類當中過載了比較函式,最後返回的是一個過載了比較函式的新的物件。這些__lt__, __gt__函式就是類當中過載的比較函式。比如__lt__是小於的判斷函式,__eq__是相等的函式。那麼問題來了,我們能不能直接在Kid類當中過載比較函式呢,這樣就可以直接排序了。

答案是確定的,我們當然可以這麼辦,實際上這也是物件導向當中非常常用的做法。相比於自定義比較函式,我們往往更傾向於在類當中定義好優先順序。Python當中實現的方法也很簡單,就是我們手動實現一個__lt__函式,sorted預設會將小的元素排在前面,所以我們只用實現__lt__一個函式就夠了。這個函式當中傳入的引數是另一個物件,我們直接在函式里面寫清楚比較邏輯就行了。返回True表示當前物件比other小,否則比other大。

我們附上完整程式碼:

#學習中遇到問題沒人解答?小編建立了一個Python學習交流群:531509025
class Kid:
    def __init__(self, name, score, age):
        self.name = name
        self.score = score
        self.age = age

    def __repr__(self):
        return 'Kid, name: {}, score: {}, age:{}'.format(self.name, self.score, self.age)

    def __lt__(self, other):
        return self.score > other.score or (self.score == other.score and self.age < other.age)

實現了比較函式之後,我們直接呼叫sorted,不用任何其他傳參就可以對它進行排序了。

相關文章