2.37. 具名元組與雙端佇列

串列、元組、字典與集合涵蓋了大多數的資料需求。collections 模組中另有三種容器,適合內建型別處理起來笨拙的特定問題。

2.37.1. namedtuple——無需類別的具型別記錄

普通元組依位置儲存值。對於小型、短暫存活的 2 或 3 元組來說這沒問題,但超過這個範圍後,point[0]point[1] 就開始無法說明它們究竟存了什麼。collections.namedtuple() 會回傳一個新的元組子類別,其欄位帶有名稱:

>>> from collections import namedtuple
>>> Reading = namedtuple('Reading', ('temp', 'humidity', 'ts'))
>>> r = Reading(22.5, 41.0, 137204)
>>> r.temp
22.5
>>> r.humidity
41.0
>>> r[0]
22.5

fields 引數是一個名稱字串的序列(或在 CPython 中為一個以空白分隔的字串;MicroPython 較為嚴格——請傳入元組或串列)。

為什麼要用 namedtuple 而不是類別?

  • 它是一個元組。迭代、解包、相等性比較、雜湊,以及作為字典鍵使用,全都免費可用。

  • 它是不可變的。重新指派 r.temp = ... 會引發 AttributeError,這正是你對記錄型別所期望的行為。

  • 它比具有相同欄位的類別實例耗用更少的 RAM——元組的儲存是連續的,沒有 __dict__

與等效的類別相比,namedtuple 宣告只有一行。其取捨在於欄位是唯讀的——若要「更改」一筆讀值,你得建立一筆新的。

2.37.2. deque——有界的環形緩衝區

串列在「尾端」很快(append / pop),在「開頭」則很慢(insert(0, ...) / pop(0) 都會移動其他每一個元素)。collections.deque 在兩端都很快——它是一個由頭尾指標索引的環形緩衝區,因此無論 deque 持有多少項目,在任一端的 append 與 pop 都以相同的固定工作量執行。

在 MicroPython 中建構時,需要依序提供一個初始可迭代物件「以及」一個最大長度:

>>> from collections import deque
>>> events = deque((), 5)
>>> for i in range(8):
...     events.append(i)
>>> list(events)
[3, 4, 5, 6, 7]

當有界的 deque 滿了時,每次 append 都會丟棄最舊的項目。這使得 deque 非常適合「最近 N 個樣本」、「最近 N 行日誌」,或任何不需要無限增長的滾動視窗。

MicroPython 的 deque 所公開的方法刻意維持精簡:

  • append(x)——加到右端。

  • appendleft(x)——加到左端。

  • extend(iterable)——將可迭代物件中的每個項目逐一附加。

  • pop()——移除並回傳右端。在空時引發 IndexError

  • popleft()——移除並回傳左端。

相較於 CPython 的 deque,值得注意的省略:沒有 clearcountindexremovereverserotatemaxlen 屬性或 __contains__。迭代與下標索引可以運作:

>>> events[0]
3
>>> for e in events:
...     print(e)

一個典型的用途:保留最近幾筆感測器讀值以偵測趨勢變化:

history = deque((), 10)

def push(reading):
    history.append(reading)
    if len(history) == 10 and history[-1] > 2 * history[0]:
        print('reading is climbing')

2.37.3. OrderedDict——當順序是相等性的一部分

自 MicroPython 1.13 與 CPython 3.7 起,一般的 dict 已保留插入順序。這涵蓋了人們過去之所以求助於 collections.OrderedDict 的最常見原因。

OrderedDict 仍能提供、而普通字典做不到的是:

  • OrderedDict 的相等性會考量順序。兩個普通字典只要擁有相同的鍵/值對,無論插入順序為何都比較相等。兩個 OrderedDict 實例則只有在它們的配對順序相同時才相等:

    >>> from collections import OrderedDict
    >>> OrderedDict([('a', 1), ('b', 2)]) == OrderedDict([('b', 2), ('a', 1)])
    False
    >>> {'a': 1, 'b': 2} == {'b': 2, 'a': 1}
    True
    
  • 當你要把設定序列化成一個「確實」在意鍵順序的格式(TOML、某些 YAML 消費端)時,OrderedDict 很有用;或者當你想給程式碼的讀者一個清楚的文件提示、表明順序很重要時,它也很有用。

對於日常程式碼,請優先使用內建的 dict。只有在「順序是相等性的一部分」的語意,或文件上的價值確實帶來好處時,才求助於 OrderedDict

這三種容器各有一個狹窄的適用情境。具名元組取代手工打造的記錄類別;deque 在有界佇列上取代串列;ordered dict 讓插入順序成為契約的一部分。