【Python HOWTOs系列】排序
Author: | Andrew Dalke and Raymond Hettinger |
---|---|
Release: | 0.1 |
Python 列表有內建就地排序的方法 list.sort(),此外還有一個內建的 sorted() 函式將一個可迭代物件(iterable)排序為一個新的有序列表。
本文我們將去探索用 Python 做資料排序的多種方法。
排序基礎
簡單的升序排序非常容易:只需呼叫 sorted() 函式,就得到一個有序的新列表:
1 2 |
>>> sorted([5, 2, 3, 1, 4]) [1, 2, 3, 4, 5] |
你也可以使用 list.sort() 方法,此方法為就地排序(並且返回 None 來避免混淆)。通常來說這不如 sorted() 方便——但是當你不需要保留原始列表的時候,這種方式略高效一些。
1 2 3 4 |
>>> a = [5, 2, 3, 1, 4] >>> a.sort() >>> a [1, 2, 3, 4, 5] |
另外一個區別是 list.sort() 方法只可以供列表使用,而 sorted() 函式可以接受任意可迭代物件(iterable)。
1 2 |
>>> sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'}) [1, 2, 3, 4, 5] |
Key 函式
list.sort() 和 sorted() 都有一個 key 引數,用於指定在作比較之前,呼叫何種函式對列表元素進行處理。 For example, here’s a case-insensitive string comparison: 例如,忽略大小寫的字串比較:
1 2 |
>>> sorted("This is a test string from Andrew".split(), key=str.lower) ['a', 'Andrew', 'from', 'is', 'string', 'test', 'This'] |
key 引數的值應該是一個函式,該函式接收一個引數,並且返回一個 key 為排序時所用。這種方法速度很快,因為每個輸入項僅呼叫一次 key 函式。
一種常見模式是使用物件的下標作為 key 來排序複雜物件。例如:
1 2 3 4 5 6 7 |
>>> student_tuples = [ ('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10), ] >>> sorted(student_tuples, key=lambda student: student[2]) # sort by age [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] |
同樣的技巧也可以用在帶有命名屬性(named attributes)的物件上。例如:
1 2 3 4 5 6 7 |
>>> class Student: def <strong>init</strong>(self, name, grade, age): self.name = name self.grade = grade self.age = age def <strong>repr</strong>(self): return repr((self.name, self.grade, self.age)) |
1 2 3 4 5 6 7 |
>>> student_objects = [ Student('john', 'A', 15), Student('jane', 'B', 12), Student('dave', 'B', 10), ] >>> sorted(student_objects, key=lambda student: student.age) # sort by age [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] |
上述的 key 函式模式是非常常見的,所以 Python 提供了一些更簡單快速的訪問屬性的函式。operator 模組有 itemgetter()、attrgetter() 和 methodcaller() 函式。 Using those functions, the above examples become simpler and faster: 使用這些函式,可以使上述的示例更加簡潔高效:
1 |
>>> from operator import itemgetter, attrgetter |
1 2 |
>>> sorted(student_tuples, key=itemgetter(2)) [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] |
1 2 |
>>> sorted(student_objects, key=attrgetter('age')) [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] |
operator 模組方法允許多級排序。例如,可以先按 grade 排序,然後再按 age 排序:
1 2 |
>>> sorted(student<em>tuples, key=itemgetter(1,2)) [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]</em> |
1 2 |
>>> sorted(student_objects, key=attrgetter('grade', 'age')) [('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)] |
list.sort() 和 sorted() 都有布林型的 reverse 引數,用來指定是否降序。例如,按 age 的降序來對學生資料進行排序:
1 2 |
>>> sorted(student_tuples, key=itemgetter(2), reverse=True) [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)] |
1 2 |
>>> sorted(student_objects, key=attrgetter('age'), reverse=True) [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)] |
排序是保證為穩定的,也就是說,當多條記錄擁有相同的 key 時,原始的順序會被保留下來。
1 2 3 |
>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)] >>> sorted(data, key=itemgetter(0)) [('blue', 1), ('blue', 2), ('red', 1), ('red', 2)] |
注意到兩條 blue 記錄保持了原來的順序, 所以 (‘blue’, 1) 一定在 (‘blue’, 2) 之前。
這個非常棒的屬性允許你通過一系列排序來進行復雜排序。例如,學生資料先按 grade 升序,然後按 age 降序,優先排序 age,然後再按 grade 排序:
1 2 3 |
>>> s = sorted(student_objects, key=attrgetter('age')) # sort on secondary key >>> sorted(s, key=attrgetter('grade'), reverse=True) # now sort on primary key, descending [('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)] |
Python 使用的 Timsort 演算法由於可以有效利用資料集中已有的順序,因而可以高效地進行多級排序。
使用 Decorate-Sort-Undecorate 的舊方法
Decorate-Sort-Undecorate 的名稱來源於這種方法的三個步驟:
- 第一步,初始的列表進行轉換,獲得用於排序的新值。
- 第二步,將轉換為新值的列表進行排序。
- 最後,還原資料並得到一個排序後僅包含原始值的列表。
例如,使用 DSU(譯註:Decorate-Sort-Undecorate的簡寫)方法,按 grade 來排序學生資料:
1 2 3 4 |
>>> decorated = [(student.grade, i, student) for i, student in enumerate(student_objects)] >>> decorated.sort() >>> [student for grade, i, student in decorated] # undecorate [('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)] |
這一方法利用了元組按字典序 (lexicographically) 比較的特性;先比較第一項;如果第一項相同,則比較第二項,以此類推。
在很多情況下是不需要在處理後的列表(decorated list)包含原始下標 i,但是包含原始下標有兩個好處:
- 排序是穩定的——如果有兩項有相同的 key,排序後的列表會保留他們的順序。
- 原始項不需要是可比較的,因為處理後的元組最多使用前面兩項就可以決定排序。例如,原始列表中包含無法直接比較的複數。
這個方法還有另外一個名字,是以 Randal L. Schwartz 的名字來命名的 Schwartzian 變換,因為他使得這個變換在 Perl 程式設計師中得以流行。
在 Python 排序提供 key 函式之後,這個技巧已經不常用了。
使用 cmp 引數的舊方法
本篇指南中給出的方法都假設 Python 2.4 或更新版本。在 2.4 之前,sorted() 和 list.sort() 是沒有 key 引數的。但是,在所有的 Py2.x 版本都支援 cmp 引數來處理使用者自定義排序函式。
在 Py3.0 中,cmp 引數已經被完全移除(作為簡化和統一語言的一部分,去除排序和 cmp() 魔法方法之間的衝突)。
在 Py2.x 中,sort 允許傳入一個可選函式,會在進行比較的時候呼叫。函式必須接受兩個引數進行比較,然後返回負數表示小於,返回 0 表示相等,返回正數表示大於。例如,我們可以這樣:
1 2 3 4 |
>>> def numeric_compare(x, y): return x - y >>> sorted([5, 2, 4, 1, 3], cmp=numeric_compare) [1, 2, 3, 4, 5] |
或者你也可以反轉比較順序:
1 2 3 4 |
>>> def reverse_numeric(x, y): return y - x >>> sorted([5, 2, 4, 1, 3], cmp=reverse_numeric) [5, 4, 3, 2, 1] |
當從 Python 2.x 移植程式碼到 3.x 時,可能會出現需要將使用者提供的排序函式轉換為 key 函式的情況。下面的包裝器可以輕鬆做到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def cmp<em>to<em>key(mycmp): 'Convert a cmp= function into a key= function' class K: def <strong>init</strong>(self, obj, *args): self.obj = obj def <strong>lt</strong>(self, other): return mycmp(self.obj, other.obj) < 0 def <strong>gt</strong>(self, other): return mycmp(self.obj, other.obj) > 0 def <strong>eq</strong>(self, other): return mycmp(self.obj, other.obj) == 0 def <strong>le</strong>(self, other): return mycmp(self.obj, other.obj) <= 0 def <strong>ge</strong>(self, other): return mycmp(self.obj, other.obj) >= 0 def __ne</em></em>(self, other): return mycmp(self.obj, other.obj) != 0 return K |
轉換為 key 函式,僅需要包裝舊的比較函式即可:
1 2 |
>>> sorted([5, 2, 4, 1, 3], key=cmp_to_key(reverse_numeric)) [5, 4, 3, 2, 1] |
在 Python 3.2 中,functools.cmp_to_key() 函式已經新增到標準庫的 functools 模組中。
其他要點
- 針對時區相關排序,使用 locale.strxfrm() 作為 key 函式,或者使用 locale.strcoll() 作為比較函式。
- reverse 引數仍然保持排序穩定性(以便相同 key 的項保留原順序)。有趣的是,無需傳入引數,通過兩次呼叫內建的 reversed() 函式,可以模擬出相同的效果:
12>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]>>> assert sorted(data, reverse=True) == list(reversed(sorted(reversed(data))))
- 在兩個物件進行比較時,sort 使用的是 lt() 方法。所以,只需要為類新增 lt() 方法,就可以為類加入排序順序:
123>>> Student.<strong>lt</strong> = lambda self, other: self.age < other.age>>> sorted(student_objects)[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
- key 函式不需要直接依賴於排序的物件。key 函式可以訪問外部資源。例如,如果學生的成績儲存在字典中,字典中的資料可以給單獨的一個學生名字排序:
1234>>> students = ['dave', 'john', 'jane']>>> newgrades = {'john': 'F', 'jane':'A', 'dave': 'C'}>>> sorted(students, key=newgrades.<strong>getitem</strong>)['jane', 'dave', 'john']