Python 列表推導及優先順序佇列的實現

goodspeed發表於2019-01-10

這一篇是《流暢的 python》讀書筆記。主要介紹列表、列表推導有關的話題,最後演示如何用列表實現一個優先順序佇列。

Python 內建序列型別

Python 標準庫用 C 實現了豐富的序列型別:

容器序列:

list、tuple和 collections.deque 這些序列能存放不同型別的資料。

扁平序列:

str、bytes、bytearray、memoryview 和 array.array,這類序列只能容納一種型別。

容器序列存放的是它們所包含的任意型別的物件的引用,而扁平序列裡存放的是值而不是引用(也可以說扁平序列其實存放的是一段連續的記憶體空間)。

如果按序列是否可被修改來分類,序列分為可變序列不可變序列:

可變序列

list、bytearray、array.array、collections.deque 和 memoryview。

不可變序列

tuple、str和 bytes。

下圖顯示了可變序列(MutableSequence)和不可變序列(sequence)的差異:

可變序列(MutableSequence)和不可變序列(sequence)的差異
可變序列(MutableSequence)和不可變序列(sequence)的差異

從這個圖可以看出,可變序列從不可變序列那裡繼承了一些方法。

列表推導和生成器表示式

列表(list)是 Python 中最基礎的序列型別。list 是一個可變序列,並且能同時存放不同型別的元素。
列表的基礎用法這裡就不再介紹了,這裡主要介紹一下列表推導。

列表推導和可讀性

列表推導是構建列表的快捷方式,並且有更好的可讀性。
先看下面兩段程式碼:

#1. 把一個字串變成 unicode 碼位的列表

>>> symbols = `$&@#%^&*`
>>> codes = []
>>> for symbol in symbols:
        codes.append(ord(symbol))

>>> codes
[36, 38, 64, 35, 37, 94, 38, 42]複製程式碼

#2. 把一個字串變成 unicode 碼位的列表 使用列表推導

>>> symbols = `$&@#%^&*`
>>> codes = [ord(s) for s in symbols]
>>> codes
[36, 38, 64, 35, 37, 94, 38, 42]複製程式碼

對比發現,如果理解列表推導的話,第二段程式碼比第一段更簡潔可讀性也更好。
當然,列表推導也不應該被濫用,通常的原則是只用列表推導來建立新的列表,並且儘量保持簡短。
如果列表推導超過兩行,就應該考慮要不要使用 for 迴圈重寫了。

NOTE

在 Python2 中列表推導有變數洩露的問題

#Python2 的例子

>>> x = `my precious`
>>> dummy = [x for x in `ABC`]
>>> x
`C`複製程式碼

這裡 x 原來的值被取代了,變成了列表推導中的最後一個值,需要避免這個問題。好訊息是 Python3解決了這個問題。

#Python3 的例子

>>> x = `ABC`
>>> dummy = [ord(x) for x in x]
>>> x 
`ABC`
>>> dummy
[65, 66, 67]複製程式碼

可以看到,這裡 x 原有的值被保留了,列表推導也建立了正確的列表。

笛卡爾積

列表推導還可以生成兩個或以上的可迭代型別的笛卡爾積。

笛卡爾積是一個列表,列表裡的元素是由輸入的可迭代型別的元素對構成的元組,因此笛卡爾積列表的長度等於輸入變數的長度的成績,如圖所示:

笛卡爾積
笛卡爾積

# 使用列表推導計算笛卡爾積程式碼如下

>>> suits = [`spades`, `diamonds`, `clubs`, `hearts`]
>>> nums = [`A`, `K`, `Q`]
>>> cards = [(num, suit) for num in nums for suit in suits]
>>> cards
[(`A`, `spades`),
 (`A`, `diamonds`),
 (`A`, `clubs`),
 (`A`, `hearts`),
 (`K`, `spades`),
 (`K`, `diamonds`),
 (`K`, `clubs`),
 (`K`, `hearts`),
 (`Q`, `spades`),
 (`Q`, `diamonds`),
 (`Q`, `clubs`),
 (`Q`, `hearts`)]複製程式碼

這裡得到的結果是先按數字排列,再按圖案排列。如果想先按圖案排列再按數字排列,只需要調整 for 從句的先後順序。

過濾序列元素

問題:你有一個資料序列,想利用一些規則從中提取出需要的值或者是縮短序列

最簡單的過濾序列元素的方法是使用列表推導。比如:

>>> mylist = [1, 4, -5, 10, -7, 2, 3, -1]
>>> [n for n in mylist if n >0]
[1, 4, 10, 2, 3]複製程式碼

使用列表推導的一個潛在缺陷就是若干輸入非常大的時候會產生一個非常大的結果集,佔用大量記憶體。這個時候,使用生成器表示式迭代產生過濾元素是一個好的選擇。

生成器表示式

生成器表示式遵守了迭代器協議,可以逐個產出元素,而不是先建立一個完整的列表,然後再把這個列表傳遞到某個建構函式裡。

生成器表示式的語法跟列表推導差不多,只需要把方括號換成圓括號。

# 使用生成器表示式建立列表

>>> pos = (n for n in mylist if n > 0)
>>> pos
<generator object <genexpr> at 0x1006a0eb0>
>>> for x in pos:
... print(x) 
...
1
4
10 
2 
3複製程式碼

如果生成器表示式是一個函式呼叫過程中唯一的引數,那麼不需要額外再用括號把它圍起來。例如:

tuple(n for n in mylist)複製程式碼

如果生成器表示式是一個函式呼叫過程中其中一個引數,此時括號是必須的。比如:

>>> import array
>>> array.array(`list`, (n for n in mylist))
array(`list`, [1, 4, 10, 2, 3])複製程式碼

實現一個優先順序佇列

問題

怎麼實現一個按優先順序排序的佇列?並在這個佇列上每次 pop 操作總是返回優先順序最高的那個元素

解決方法

利用 heapq 模組

heapq 是 python 的內建模組,原始碼位於 Lib/heapq.py ,該模組提供了基於堆的優先排序演算法。

堆的邏輯結構就是完全二叉樹,並且二叉樹中父節點的值小於等於該節點的所有子節點的值。這種實現可以使用 heap[k] <= heap[2k+1] 並且 heap[k] <= heap[2k+2] (其中 k 為索引,從 0 開始計數)的形式體現,對於堆來說,最小元素即為根元素 heap[0]。

可以通過 list 對 heap 進行初始化,或者通過 api 中的 heapify 將已知的 list 轉化為 heap 物件。

heapq 提供的一些方法如下:

  • heap = [] #建立了一個空堆
  • heapq.heappush(heap, item):向 heap 中插入一個元素
  • heapq.heappop(heap):返回 root 節點,即 heap 中最小的元素
  • heapq.heappushpop(heap, item):向 heap 中加入 item 元素,並返回 heap 中最小元素
  • heapq.heapify(x)
  • heapq.nlargest(n, iterable, key=None):返回可列舉物件中的 n 個最大值,並返回一個結果集 list,key 為對該結果集的操作
  • heapq.nsmallest(n, iterable, key=None):同上相反

實現如下:

import heapq
class PriorityQueue: 
    def __init__(self):
        self._queue = []
        self._index = 0

    def push(self, item, priority):
        heapq.heappush(self._queue, (-priority, self._index, item)) 
        self._index += 1

    def pop(self):
        return heapq.heappop(self._queue)[-1]複製程式碼

下面是它的使用方法:

>>> class Item:
        def __init__(self, name):
            self.name = name
        def __repr__(self):
            return `Item({!r})`.format(self.name)

>>> q = PriorityQueue()
>>> q.push(Item(`foo`), 1)
>>> q.push(Item(`bar`), 5)
>>> q.push(Item(`spam`), 4)
>>> q.push(Item(`grok`), 1)
>>> q.pop()
Item(`bar`) 
>>> q.pop() 
Item(`spam`) 
>>> q.pop() 
Item(`foo`) 
>>> q.pop() 
Item(`grok`)複製程式碼

通過執行結果我們可以發現,第一個 pop() 操作返回優先順序最高的元素。兩個優先順序相同的元素(foo 和 grok),pop 操作按照它們被插入到佇列的順序返回。

函式 heapq.heappush() 和 heapq.heappop() 分別在佇列 queue 上插入和刪除第一個元素,並且佇列 queue 保證 第一個元素擁有最小優先順序。 heappop() 函式總是返回 最小的 的元素,這就是保證佇列 pop 操作返回正確元素的關鍵。另外,由於 push 和 pop 操作時間複雜度為 O(log N),其中 N 是堆的大小,因此就算是 N 很大的時候它們 執行速度也依舊很快。
在上面程式碼中,佇列包含了一個 (-priority, index, item) 的元組。優先順序為負 數的目的是使得元素按照優先順序從高到低排序。這個跟普通的按優先順序從低到高排序的堆排序恰巧相反。
index 變數的作用是保證同等優先順序元素的正確排序。通過儲存一個不斷增加的 index 下標變數,可以確保元素按照它們插入的順序排序。而且, index 變數也在相 同優先順序元素比較的時候起到重要作用。

實現上邊排序的關鍵是 元組是支援比較的:

>>> a = (1, Item(`foo`)) 
>>> b = (5, Item(`bar`)) 
>>> a < b
True
>>> c = (1, Item(`grok`))
>>> a < c
Traceback (most recent call last):
File "<stdin>", line 1, in <module> 
TypeError: unorderable types: Item() < Item()複製程式碼

當第一個值大小相等時,由於Item 並不支援比較會丟擲 TypeError。為了避免上述錯誤,我們引入了index(不可能用兩個元素有相同的 index 值), 變數組成了(priority, index, item) 三元組。現在再比較就不會出現上述問題了:

>>> a = (1, 0, Item(`foo`)) 
>>> b = (5, 1, Item(`bar`)) 
>>> c = (1, 2, Item(`grok`)) 
>>> a < b
True
>>> a < c 
True複製程式碼

主要介紹列表、列表推導有關的話題,最後演示如何用heapq列表實現一個優先順序佇列。下一篇介紹元組

參考連結


最後,感謝女朋友支援。

歡迎關注(April_Louisa) 請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章