一文帶你解密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其它相關文章!
推薦閱讀:
- python進階從青銅到王者一定會用上的Python技巧
- Python中非常好用的內置函數詳解
- python3中dict.keys().sort()用不瞭的解決方法
- Python一行代碼可直接使用最全盤點
- Python列表排序 list.sort方法和內置函數sorted用法