一文帶你解密Python可迭代對象的排序問題

假設有一個可迭代對象,現在想要對它內部的元素進行排序,我們一般會使用內置函數 sorted,舉個例子:

data = (3, 4, 1, 2, 5)
print(
    sorted(data)
)  # [1, 2, 3, 4, 5]

data = (3.14, 2, 1.75)
print(
    sorted(data)
)  # [1.75, 2, 3.14]

data = ["satori", "koishi", "marisa"]
print(
    sorted(data)
)  # ['koishi', 'marisa', 'satori']

如果可迭代對象裡面的元素是數值,那麼會按照數值的大小進行排序;如果是字符串,那麼會按照字符串的字典序進行排序,並且 sorted 函數會返回一個新的列表。

sorted 函數默認是升序排序,如果想要降序,那麼可以傳遞一個關鍵字參數 reverse=True。

data = [(3, 4),
        (3, 1),
        (2, 3)]

print(
    sorted(data)
)  # [(2, 3), (3, 1), (3, 4)]

如果可迭代對象裡面都是元組的話,也是可以的,元組在比較大小的時候,會先按照元組的第一個元素比較;第一個元素相等,再按照第二個元素比較,依次類推。

因此在使用 sorted 函數的時候,可迭代對象內部的元素,要滿足彼此之間都是可比較的,否則報錯。

data = [123, 456, "123"]

try:
    print(sorted(data))
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'str' and 'int'
"""

data = [{"a": 1}, {"b": 2}]
try:
    print(sorted(data))
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'dict' and 'dict'
"""

我們看到,由於 data 裡面存在不可比較的元素,因此報錯瞭。那麼問題來瞭,假設有這樣一個列表:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 15},
        {"name": "koishi", "age": 16}]

# 字典是不可比較大小的,因此直接使用 sorted 會報錯
# 我們希望按照字典內部的 "age" 字段進行排序
# 得到下面的結果

[{'name': 'marisa', 'age': 15},
 {'name': 'koishi', 'age': 16},
 {'name': 'satori', 'age': 17}]

如果是你的話,你會怎麼做呢?很明顯,我們將每個 "age" 字段的值取出來,和所在的字典拼接成一個元組(或列表)不就行瞭,然後對元組進行排序,舉個例子:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 15},
        {"name": "koishi", "age": 16}]

data = [(d["age"], d) for d in data]
print(data)
"""
[(17, {'name': 'satori', 'age': 17}), 
 (15, {'name': 'marisa', 'age': 15}), 
 (16, {'name': 'koishi', 'age': 16})]
"""

# 由於 data 內部的元素是一個元組
# 所以排序的時候會按照元組的第一個元素排序
sorted_data = sorted(data)
print(sorted_data)
"""
[(15, {'name': 'marisa', 'age': 15}), 
 (16, {'name': 'koishi', 'age': 16}), 
 (17, {'name': 'satori', 'age': 17})]
"""

# 此時順序就排好瞭,然後再把字典取出來
sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[{'name': 'marisa', 'age': 15}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

顯然這樣就實現瞭基於字典內部某個字段的值,來對字典進行排序,隻不過上面的代碼還有一點點缺陷。我們說元組在比較的時候會先比較第一個元素,第一個元素相同的話,會比較第二個元素。

而我們上面 data 裡面的元組,由於第一個元素都不相等,所以直接就比較出來瞭。但如果是下面這種情況呢?

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]

data = [(d["age"], d) for d in data]
print(data)
"""
[(17, {'name': 'satori', 'age': 17}), 
 (16, {'name': 'marisa', 'age': 16}), 
 (16, {'name': 'koishi', 'age': 16})]
"""
try:
    sorted_data = sorted(data)
except TypeError as e:
    print(e)
"""
'<' not supported between instances of 'dict' and 'dict'
"""

此時就報錯瞭,因為第二個元組和第三個元組內部的第一個元素都是 16,所以第一個元素相等,那麼會比較第二個元素。而第二個元素是字典,字典之間無法比較,所以報錯瞭。

但我們隻是希望讓字典的 "age" 字段的值參與比較,如果相等的話,那麼就不用再比較瞭,相對順序就保持現狀。所以我們可以這麼做:

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]

# 我們將索引也加進去
data = [(d["age"], i, d) for i, d in enumerate(data)]
print(data)
"""
[(17, 0, {'name': 'satori', 'age': 17}), 
 (16, 1, {'name': 'marisa', 'age': 16}), 
 (16, 2, {'name': 'koishi', 'age': 16})]
"""

# 如果 "age" 字段的值、或者說元組的第一個元素相等
# 那麼就按照索引比較,索引一定是不重復的
sorted_data = sorted(data)
print(sorted_data)
"""
[(16, 1, {'name': 'marisa', 'age': 16}), 
 (16, 2, {'name': 'koishi', 'age': 16}), 
 (17, 0, {'name': 'satori', 'age': 17})]
"""

# 此時就成功排好序瞭
# 並且 "age" 字段的值相等的字典之間的相對順序
# 在排序之前和排序之後都保持一致
# 這正是我們想要的結果
sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[{'name': 'marisa', 'age': 16}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

再比如,我們想要對元組排序,但我們希望按照元組的第二個元素進行排序:

data = [("satori", 17),
        ("marisa", 15),
        ("koishi", 16)]

data = [(tpl[1], i, tpl) for i, tpl in enumerate(data)]
print(data)
"""
[(17, 0, ('satori', 17)), 
 (15, 1, ('marisa', 15)), 
 (16, 2, ('koishi', 16))]
"""

sorted_data = sorted(data)
print(sorted_data)
"""
[(15, 1, ('marisa', 15)), 
 (16, 2, ('koishi', 16)), 
 (17, 0, ('satori', 17))]
"""

sorted_data = [tpl[-1] for tpl in sorted_data]
print(sorted_data)
"""
[('marisa', 15), 
 ('koishi', 16), 
 ('satori', 17)]
"""

所以當可迭代對象內部的元素無法進行排序,或者說我們不希望基於整個元素進行排序,那麼就可以使用上面這個方法。將用來排序的值、索引、原始值放在一個元組裡面,然後對元組排序,排完瞭再把最後一個值(也就是原始值)篩出來即可。

或者我們還可以做的再復雜一些:

data = [-3, -2, 3, 2, -1, 1, 0]

"""
對 data 進行排序,排序規則如下
先按照內部元素的正負進行排序,排序之後正數在後面
如果符號一樣,再按照絕對值的大小進行排序
也就是說,排完之後是下面這樣一個結果

[-1, -2, -3, 0, 1, 2, 3]
"""

# 如果隻按照正負排序
data1 = [(n >= 0, i, n) for i, n in enumerate(data)]
sorted_data = sorted(data1)
print(sorted_data)
"""
[(False, 0, -3), (False, 1, -2), (False, 4, -1), 
 (True, 2, 3), (True, 3, 2), (True, 5, 1), (True, 6, 0)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 此時正數就排在瞭負數的後面
print(
    sorted_data
)  # [-3, -2, -1, 3, 2, 1, 0]


# 如果隻按照絕對值排序
data2 = [(abs(n), i, n) for i, n in enumerate(data)]
sorted_data = sorted(data2)
print(sorted_data)
"""
[(0, 6, 0), (1, 4, -1), (1, 5, 1), 
 (2, 1, -2), (2, 3, 2), (3, 0, -3), (3, 2, 3)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 此時絕對值大的數,就排在瞭絕對值小的數的後面
print(
    sorted_data
)  # [0, -1, 1, -2, 2, -3, 3]


# 同時按照正負和絕對值排序
data3 = [(n >= 0, abs(n), i, n) for i, n in enumerate(data)]
sorted_data = sorted(data3)
print(sorted_data)
"""
[(False, 1, 4, -1), (False, 2, 1, -2), 
 (False, 3, 0, -3), (True, 0, 6, 0), 
 (True, 1, 5, 1), (True, 2, 3, 2), (True, 3, 2, 3)]
"""
sorted_data = [tpl[-1] for tpl in sorted_data]
# 大功告成
print(
    sorted_data
)  # [-1, -2, -3, 0, 1, 2, 3]

那麼接下來,我們就可以封裝一個屬於我們自己的 my_sorted 函數瞭。

def my_sorted(data, *, key=None, reverse=False):
    """
    :param data: 可迭代對象
    :param key: callable
    :param reverse: 是否逆序
    :return:
    """
    if key is not None:
        data = [(key(item), i, item)
                for i, item in enumerate(data)]
    sorted_data = sorted(data)
    if key is not None:
        sorted_data = [tpl[-1] for tpl in sorted_data]
    if reverse:
        sorted_data = sorted_data[:: -1]
    return sorted_data

# 下面來測試一下
data = [-3, -2, 3, 2, -1, 1, 0]
print(
    my_sorted(data, key=lambda x: (x >= 0, abs(x)))
)  # [-1, -2, -3, 0, 1, 2, 3]

data = [{"name": "satori", "age": 17},
        {"name": "marisa", "age": 16},
        {"name": "koishi", "age": 16}]
print(my_sorted(data, key=lambda x: x["age"]))
"""
[{'name': 'marisa', 'age': 16}, 
 {'name': 'koishi', 'age': 16}, 
 {'name': 'satori', 'age': 17}]
"""

結果一切正常,當然啦,實際工作中我們肯定不會專門封裝一個 my_sorted 函數,因為內置的 sorted 已經包含瞭我們上面的所有功能。

data = [-3, -2, 3, 2, -1, 1, 0]
print(
    sorted(data, key=lambda x: (x >= 0, abs(x)))
)  # [-1, -2, -3, 0, 1, 2, 3]

print(
    sorted(data, key=lambda x: (x >= 0, abs(x)), reverse=True)
)  # [3, 2, 1, 0, -3, -2, -1]

內置函數 sorted 除瞭接收一個可迭代對象之外,還接收兩個關鍵字參數 key 和 reverse,含義就是我們介紹的那樣。在 sorted 的內部,它的處理方式和我們上面是一致的,如果指定瞭 key,也就是自定義排序規則,那麼在底層會將可迭代對象內部的值封裝成元組,然後對元組排序。排完序之後,再將元組的最後一個值、也就是原始值取出來,並返回。

所以這就是 sorted 函數的全部秘密,它裡面的參數 key 賦予瞭 sorted 函數強大的能力,有瞭這個參數,我們想怎麼排序,就怎麼排序。

class A:

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

    def __repr__(self):
        return f"self.a = {self.a}"

    def __hash__(self):
        return self.a

a1 = A(1)
a2 = A(2)
a3 = A(3)
a4 = A(4)

data = [a2, a3, a1, a4]
print(data)
"""
[self.a = 2, self.a = 3, self.a = 1, self.a = 4]
"""

# A 的實例對象無法比較,我們希望按照內部的屬性 a 進行比較
print(sorted(data, key=lambda x: x.a))
"""
[self.a = 1, self.a = 2, self.a = 3, self.a = 4]
"""

# 或者按照哈希值比較,此時仍相當於按照 self.a 比較
print(sorted(data, key=lambda x: hash(x), reverse=True))
"""
[self.a = 4, self.a = 3, self.a = 2, self.a = 1]
"""

因此我們想怎麼比就怎麼比,參數 key 賦予瞭我們極大的自由,key 接收一個函數(當然其它 callable 也可以,但大部分場景都是匿名函數),此函數接收一個參數,該參數會對應可迭代對象裡面的每一個元素。而函數的返回值,決定瞭 sorted 的比較邏輯。

比如,我們不光可以對元組、列表排序,還可以對字典內部的鍵值對排序。

d = {"satori": 17, "marisa": 15, "koishi": 16}

# 對字典調用 sorted,針對的是字典裡面的鍵
# 所以返回的也是鍵
print(sorted(d))  # ['koishi', 'marisa', 'satori']

# 匿名函數裡面的參數 x 對應可迭代對象裡面的每一個元素
# 這裡就是字典的鍵,函數返回 d[x] 表示按照值來排序
# 但排序之後得到的仍然是鍵
print(
    sorted(d, key=lambda x: d[x])
)  # ['marisa', 'koishi', 'satori']

# 此時的 x 就是鍵值對組成的元組
# 這裡按照值來排序
print(
    sorted(d.items(), key=lambda x: x[1])
)  # [('marisa', 15), ('koishi', 16), ('satori', 17)]

# 如果排序的時候,還希望鍵值對調換順序,可以這麼做
print(
    sorted(zip(d.values(), d.keys()), key=lambda x: x[0])
)  # [(15, 'marisa'), (16, 'koishi'), (17, 'satori')]

當然啦,還有很多其它排序方式,比如按照數量排序:

string = "a" * 4 + "b" * 3 + "c" * 5 + "d" * 2
data = ["a", "b", "c", "d"]

print(
    sorted(data, key=lambda x: string.count(x))
)  # ['d', 'b', 'a', 'c']

最後再來介紹一個知識點,sorted 在對可迭代對象內部的元素進行排序的時候,肯定要有大小比較的過程,這是肯定的。但問題是比較的時候,用的什麼方式呢?舉個例子,我想判斷 a 和 b 的大小關系(假設不相等),無論是執行 a > b 還是 a < b,根據結果我都能得出它們誰大誰小。

而 sorted 在比較的時候是怎麼做的呢,這裡給出結論:每次在比較兩個對象的時候,都會調用左邊對象的 __lt__ 方法。其實關於 sorted 內部是怎麼比的,我們無需太關註,但之所以說這一點,是因為在極端場景下可能會遇到。舉個例子:

# 第一個元素表示 "商品名稱"
# 第二個元素表示 "銷量"
data = [("apple", 200),
        ("banana", 200),
        ("peach", 150),
        ("cherry", 150),
        ("orange", 150)]

# 我們需要先按照 "銷量" 的大小降序排序
# 如果 "銷量" 相同,則按照 "商品名稱" 的字典序升序排序
# 該怎麼做呢?

# 由於一部分升序,一部分降序
# 我們無法直接使用 reverse 參數,所以就默認按照升序排
# 雖然 "銷量" 要求降序排,但可以對它取反
# 這樣值越大,取反之後的值就越小,從而實現降序效果
print(
    sorted(data, key=lambda x: (~x[1], x[0]))
)
"""
[('apple', 200), 
 ('banana', 200), 
 ('cherry', 150), 
 ('orange', 150), 
 ('peach', 150)]
"""

可能有小夥伴覺得這也沒什麼難的,那麼我們將問題稍微換一下。如果讓你先按照 "銷量" 升序排序,如果 "銷量相同",再按照 "商品名稱" 的字典序降序排序,你要怎麼做呢?

顯然這個問題的難點就在於字符串要怎麼降序排,整數可以取反,但字符串是無法取反的。所以我們可以自定義一個類,實現它的 __lt__ 方法。

data = [("apple", 200),
        ("banana", 200),
        ("peach", 150),
        ("cherry", 150),
        ("orange", 150)]


class STR(str):

    def __lt__(self, other):
        # 調用 str 的 __lt__,得到佈爾值
        # 然後再取反,當然,把 not 換成 ~ 也是可以的
        # 因此:"apple" < "banana" 為 True
        # 但是:STR("apple") < STR("banana") 為 False
        return not super().__lt__(other)


# 銷量升序排,直接 x[1] 即可
# 但是商品名稱降序排,需要使用類 STR 將 x[0] 包起來
print(
    sorted(data, key=lambda x: (x[1], STR(x[0])))
)
"""
[('peach', 150), 
 ('orange', 150), 
 ('cherry', 150), 
 ('banana', 200), 
 ('apple', 200)]
"""

# 事實上,如果你的思維夠靈活,你會發現
# "銷量"降序排、"商品名稱"升序排,排完之後再整體取反
# 就是這裡 "銷量"升序排、"商品名稱"將序排 的結果
print(
    sorted(data, key=lambda x: (~x[1], x[0]), reverse=True)
    ==
    sorted(data, key=lambda x: (x[1], STR(x[0])))
)  # True

# 當然這個思路也很巧妙

由於默認是調用 __lt__ 進行比較的,因此我們需要實現 __lt__。

以上就是一文帶你解密Python可迭代對象的排序問題的詳細內容,更多關於Python可迭代對象排序的資料請關註WalkonNet其它相關文章!

推薦閱讀: