Python:如何排序(sort)


一、前言

對Python的列表(list)有兩個用於排序的方法:

一個是內建方法list.sort(),可以直接改變列表的內容:

>>> list1 = [9,8,7,6,5]
>>> list1.sort()
>>> list1
[5, 6, 7, 8, 9]

另一個是內建函數sorted(),它的特點是不改變原列表的內容,而是根據一個可迭代對象建立一個新的列表:

>>> list2 = [4,3,2,1]
>>> list3 = sorted(list2)
>>> list2
[4, 3, 2, 1]
>>> list3
[1, 2, 3, 4]

 

二、基礎排序

最簡單的升序排序非常容易:直接調用sorted()函數就可以了,它返回一個新的列表:

>>> sorted([5, 2, 3, 1, 4])
[1, 2, 3, 4, 5]

 

也可以使用列表本身的方法list.sort()去排序。它會改變list的內容,然后返回None作為執行的結果,以避免混淆。一般來說它沒有sorted()那么方便,但是如果你不需要原來的列表的話,使用它在性能上會有輕微的提升。

>>> a = [5, 2, 3, 1, 4]
>>> a.sort()
>>> a
[1, 2, 3, 4, 5]

 

另一個區別就是,list.sort()方法只能用於列表,相對的,sorted()函數則適用於所有的可迭代對象,如:

>>> sorted({1: 'D', 2: 'B', 3: 'B', 4: 'E', 5: 'A'})
[1, 2, 3, 4, 5]

 

三、key函數

從Python2.4開始,無論是list.sort()還是sorted()都增加了一個key參數,指定一個在進行比較之前作用在每個列表元素上的函數。

例如,以下就是大小寫不敏感的字符串比較:

>>> sorted("This is a test string from Andrew".split(), key=str.lower)
['a', 'Andrew', 'from', 'is', 'string', 'test', 'This']

 

key參數對應的值,必須是這樣一個函數:接受一個參數然后返回一個用來排序的鍵。用這種技術來排序在速度上是非常快的,因為key函數恰好被每一個輸入記錄調用一次。

一種常用的模式是,在對復雜對象進行排序的時候,使用這個對象的索引作為排序的鍵,例如:

>>> 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)]

用對象的命名屬性也是一樣的效果,例如:

>>> class Student:
...     def __init__(self, name, grade, age):
...         self.name = name
...         self.grade = grade
...         self.age = age
...     def __repr__(self):
...         return repr((self.name, self.grade, self.age))

>>> 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)]

 

四、使用Operator模塊的函數

上面提到的key函數在排序中是很常用的,因此Python本身也提供了很多很方便的函數,讓創建訪問器函數變得更快、更容易。operater模塊提供的函數有operator.itemgetter(),operator.attrgetter(),另外從Python2.5開始新增了operator.methodcaller()函數。

使用這些函數,上面的例子可以變得更簡單和更快:

>>> from operator import itemgetter, attrgetter
>>> sorted(student_tuples, key=itemgetter(2))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]
>>> sorted(student_objects, key=attrgetter('age'))
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

 

operator模塊提供的幾個函數允許多級別的排序。例如,根據grade和age進行排序:

>>> sorted(student_tuples, key=itemgetter(1,2))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]
>>> sorted(student_objects, key=attrgetter('grade', 'age'))
[('john', 'A', 15), ('dave', 'B', 10), ('jane', 'B', 12)]

 

operator.methodcaller()函數會在每個被排序的對象上執行一個固定參數的方法調用,例如,str.count()方法可以通過統計在每個消息中感嘆號的數量,來計算消息的優先度:

>>> from operator import methodcaller
>>> messages = ['critical!!!', 'hurry!', 'standby', 'immediate!!']
>>> sorted(messages, key=methodcaller('count', '!'))
['standby', 'hurry!', 'immediate!!', 'critical!!!']

 

五、升序和降序

list.sort()和sorted()都接受一個reverse參數,這個參數的取值是一個布爾值,用來標記是否降序排序。例如,用age字段降序排列去獲取學生信息:

>>> sorted(student_tuples, key=itemgetter(2), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]
>>> sorted(student_objects, key=attrgetter('age'), reverse=True)
[('john', 'A', 15), ('jane', 'B', 12), ('dave', 'B', 10)]

 

六、排序穩定性及復雜排序

從Python2.2版本開始,排序都是穩定的,也就是說如果有兩個記錄他們的排序鍵相等,則排序前后他們的原始順序是固定不變的。

>>> 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字段后排。

>>> 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-undecorated老方法

這個習語是以它的三個步驟而被命名為decorate-sort-undecorate(裝飾-排序-去裝飾)的:

  • 首先,原始的列表被裝飾以生成新的值,這些值是用來控制排序順序的。
  • 然后,對被裝飾過的列表進行排序。
  • 最后,去掉裝飾,以新的順序創建一個列表,這個列表只包含原來列表中的值。

例如,使用DSU方法用學生數據中的grade字段對其進行排序:

>>> 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)]

在這里,元組是按照字典序進行比較的,首先第一項被比較,如果他們相等則對第二項進行比較,以此類推。

不是在所有的情況下都需要在被裝飾的列表中包含下標i,但是包含它會有兩個好處:

讓排序穩定——如果兩個項擁有一樣的鍵,那么他們在排序列表中的順序不會改變。

原始的項不需要是可對比的,因為被裝飾的元組的順序最多被前面兩個項決定。舉個例子,原始列表可以包含不能直接排序的復數。

這個習語的另一個名字是Schwartzian transform,以Randal L. Schwartz進行命名,因為他用Per語言推廣了這種方法。

 

對於大型的列表,以及那些計算對比關系的代價很昂貴的列表來說,在Python的2.4版本之前,DSU幾乎是排序這類列表的最快的方法。但是在2.4及之后的版本,key函數就提供了一樣的功能了。

 

八、使用cmp參數老方法

在這篇文章中提到的很多設計,其實都是Python2.4或之后的版本才給出的。在此之前,無論sorted()函數還是list.sort()方法都沒有不帶參的調用方式。相反的,所有Py2.x版本支持cmp參數,來處理用戶指定的對比函數。

在Python3中,cmp參數是完全被移除了(這是對簡化和統一這門語言作出的努力的一部分,另外也消除了富比較(rich comparisons )和__cmp()__魔術方法的沖突)。

在Python2中,sort()允許指定一個可選的函數,這個函數作為比較的用途被調用。這個函數必須帶兩個被用來比較的參數,然后返回一個值,如果小於是負數,如果相等是0,如果大於則返回正數。例如,我們可以這么做:

>>> def numeric_compare(x, y):
...     return x - y
>>> sorted([5, 2, 4, 1, 3], cmp=numeric_compare) 
[1, 2, 3, 4, 5]

或者你也可以反轉對比的順序:

>>> def reverse_numeric(x, y):
...     return y - x
>>> sorted([5, 2, 4, 1, 3], cmp=reverse_numeric) 
[5, 4, 3, 2, 1]

 

從Python2.x導入代碼到3.x的時候,如果你使用這種比較函數的時候,代碼會報錯,然后你就需要把它轉為一個key函數了。以下的包裝器可以讓這個轉化過程變得更簡單:

def cmp_to_key(mycmp):
    'Convert a cmp= function into a key= function'
    class K(object):
        def __init__(self, obj, *args):
            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
        def __ne__(self, other):
            return mycmp(self.obj, other.obj) != 0
    return K

想轉化成一個key函數,直接包裝舊的對比函數就可以了:

>>> sorted([5, 2, 4, 1, 3], key=cmp_to_key(reverse_numeric))
[5, 4, 3, 2, 1]

在Python2.7中,functools.cmp_to_key()函數已經被增加到functools模塊中了。

 

九、其他

對於浮點數的排序,使用locale.strxfrm()作為key函數,或者locale.strcoll()作為對比函數。

reverse參數依舊保持排序的穩定性。有趣的是,這個效果可以通過調用內建函數reversed()兩次進行模擬:

>>> data = [('red', 1), ('blue', 1), ('red', 2), ('blue', 2)]
>>> standard_way = sorted(data, key=itemgetter(0), reverse=True)
>>> double_reversed = list(reversed(sorted(reversed(data), key=itemgetter(0))))
>>> assert standard_way == double_reversed
>>> standard_way
[('red', 1), ('red', 2), ('blue', 1), ('blue', 2)]

為一個類創建一種標准的排序順序,只需要增加合適的富比較函數即可:

>>> Student.__eq__ = lambda self, other: self.age == other.age
>>> Student.__ne__ = lambda self, other: self.age != other.age
>>> Student.__lt__ = lambda self, other: self.age < other.age
>>> Student.__le__ = lambda self, other: self.age <= other.age
>>> Student.__gt__ = lambda self, other: self.age > other.age
>>> Student.__ge__ = lambda self, other: self.age >= other.age
>>> sorted(student_objects)
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

 

對於一般用途的比較,一般的方法建議定義全部六個富對比操作符。現在functools.total_ordering() 類裝飾器讓這一切變得更加容易實現。

key函數不一定要直接基於被排序的對象。key函數同樣可以訪問外部的資源。舉個例子,如果學生的年級被存儲在一個字典之中,那它就可以用來為另一個單獨的的學生的名字組成的列表進行排序:

>>> students = ['dave', 'john', 'jane']
>>> grades = {'john': 'F', 'jane':'A', 'dave': 'C'}
>>> sorted(students, key=grades.__getitem__)
['jane', 'dave', 'john']

 

以上內容翻譯自Python2.7.15文檔,Sorting HOW TO。

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM