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

字段参数是一个名称字符串的序列(在 CPython 中也可以是一个以空白分隔的字符串;MicroPython 更严格——请传入一个元组或列表)。

为什么使用 namedtuple 而不是类?

  • 它是元组。迭代、解包、相等比较、哈希以及用作字典键,全都可以直接使用。

  • 它是不可变的。重新赋值 r.temp = ... 会引发 AttributeError,而这正是你对记录类型所期望的。

  • 它比具有相同字段的类实例占用更少的 RAM——元组的存储是连续的,没有 __dict__

与等价的类相比,一条 namedtuple 声明只需一行。其代价是字段是只读的——要“修改”一个读数,你需要新建一个。

2.37.2. deque —— 有界环形缓冲区

列表在 末端append / pop)操作很快,而在 起始端insert(0, ...) / pop(0) 都会移动其他每一个元素)很慢。collections.deque 在两端都很快——它是一个由头指针和尾指针索引的环形缓冲区,因此无论双端队列持有多少元素,在任一端进行追加和弹出都耗费相同的固定工作量。

在 MicroPython 中构造时同时需要一个初始可迭代对象 一个最大长度,且顺序如此:

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

当一个有界双端队列已满时,每次 append 都会丢弃最旧的元素。这使得双端队列非常适合“最近 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

这三种容器各有一个狭窄的适用场景。命名元组取代手工编写的记录类;双端队列在有界队列场景下取代列表;有序字典则让插入顺序成为契约的一部分。