☰
boltons dictutils 模块全解:OrderedMultiDict(OMD)与 OneToOne、ManyToMany、FrozenDict 等映射类型实战指南
2026/10/8 23:41:09 网站建设 项目流程
  • 开发工具

【免费下载链接】boltons

🔩 Like builtins, but boltons. 250+ constructs, recipes, and snippets which extend (and rely on nothing but) the Python standard library. Nothing like Michael Bolton.

项目地址:https://gitcode.com/gh_mirrors/bo/boltons
点击查看免费下载

导读

boltons.dictutils是 boltons 项目中"更有力量的映射类型"模块,其核心是OrderedMultiDict(简称 OMD):一个在保留插入顺序的同时、允许一个键对应多个值的 dict 子类,被 docs/index.rst 定位为"高度优化的 OrderedMultiDict",并被 docs/architecture.rst 归入"更强大的多用途数据结构"类别。模块还提供一对一双向映射OneToOne、多对多映射ManyToMany、字典过滤工具subdict()以及可哈希的不可变字典FrozenDict。读完本文,你将掌握 OMD 的完整 API(add/getlist/todict/inverted/sorted/counts 等)、其基于双向链表的底层实现原理,以及其余四个工具的适用场景与用法,并能在 URL 查询参数解析、反向索引、非破坏性数据叠加等真实场景中直接落地。

本文主体内容来自 docs/dictutils.rst(Sphinx 自动文档入口)所对应的模块文档字符串与各方法 docstring(见 boltons/dictutils.py),全部示例均可在 Python 交互环境中直接复现,并有 tests/test_dictutils.py 中的测试用例佐证。

为什么需要 OMD:内置 dict 的取舍

Python 核心自带强大的映射类型dict。它追求简单与性能:一个快速的、无序的、1:1 的映射。在历史上它不保留插入顺序(注:2015 年起 PyPy 的基本 dict 已有序,2017 年 12 月起 CPython 3 的基本 dict 也已有序,见 boltons/dictutils.py),也不允许一个键存多个值。

OrderedMultiDict与内置dict形成鲜明对比:它是一个相对"极简主义"的、有序的 1:n 的 dict 子类型。dict 的几乎每个特性都被重新打磨,以在新增复杂度面前保持直观;同时还新增了类似collections.Counter的功能方法。

OMD 最大的优势是非破坏性(non-destructive):数据被加入 OMD 时,不会被重排、也不会被覆盖。这一特性让开发者可以更自由地处理数据,并能在不做任何额外工作的前提下,对"输入数据最终会落在输出的哪个位置"做出更多假设。

一个绝佳的例子是OMD.inverted()方法:它返回一个新的 OMD,把原键值互换(值作键、键作值),所有数据及其顺序在反转形式中依然完整保留。而同样的操作对内置dict或collections.OrderedDict而言是"完全错误且鲁莽"的。

快速上手:OMD 的基础用法

OMD 的构造函数与内置 dict 完全一致,整体 API 构成内置类型的一个直观超集:

>>> from boltons.dictutils import OrderedMultiDict # 或 OMD / MultiDict >>> omd = OrderedMultiDict() >>> omd['a'] = 1 >>> omd['b'] = 2 >>> omd.add('a', 3) # add() 在键 'a' 下追加一个值 >>> omd.get('a') 3 # get() 返回最近插入的值 >>> omd.getlist('a') [1, 3] # getlist() 返回该键下全部值

一些非 dict 风格的行为也值得一提,比如支持reversed():

>>> list(reversed(omd)) ['b', 'a']

注意:与其他一些 MultiDict 实现不同,本 OMD优先返回最近插入的值——omd['a']指向3而不是1:

>>> omd OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)]) >>> omd.poplast('a') # 弹出最近插入的值 3 >>> omd OrderedMultiDict([('a', 1), ('b', 2)]) >>> omd.pop('a') # pop 返回最近插入的值,但删除该键全部值 1 >>> omd OrderedMultiDict([('b', 2)])

上面的poplast('a')弹出最近插入的3后,键'a'仍保留值1;而pop('a')返回最近值1后,整个键及其全部值都被移除。若想得到一个"安全可修改"或"扁平"的字典,请使用todict():

>>> from pprint import pprint as pp # 保持打印顺序 >>> omd = OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)]) >>> pp(omd.todict()) {'a': 3, 'b': 2} >>> pp(omd.todict(multi=True)) {'a': [1, 3], 'b': [2]}

multi=False(默认)时,每个键以首次插入的顺序出现,值为该键最近插入的值:

>>> OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)]).items(multi=False) [('a', 3), ('b', 2)]

关于dict(omd)的跨版本行为警告

模块文档明确给出一个warning:dict(omd)的行为在Python 3.7发生了改变(因 CPython 从collections.OrderedDict过渡到内置字典有序)。3.7 之前,结果是新字典、值为列表(类似于omd.todict(multi=True),但只是浅拷贝,列表直接引用 OMD 内部结构);从 3.7 起,值变成单值(类似于omd.todict(multi=False))。为了可靠的跨版本行为,请直接使用omd.todict()。

OMD 完整 API 与源码级原理解析

底层实现:双向链表 + 索引映射

从源码结构看,OMD 的插入顺序由一棵双向链表维护,每条链上节点(cell)是一个四元素列表,节点字段通过模块级常量定位:

PREV, NEXT, KEY, VALUE, SPREV, SNEXT = range(6) # boltons/dictutils.py 第 80 行

_insert()(dictutils.py)在链表尾部追加[last, root, k, v]节点;_remove_all()(dictutils.py)则通过改写前后节点的指针完成摘除。正因底层是链表,插入/删除只需 O(1) 指针操作,__reversed__也能从root[PREV]起反向遍历(dictutils.py)。

该模块还包含一个基于**跳表(skip list)**的变体FastIterOrderedMultiDict(dictutils.py):按键迭代更快且使用常量内存,但插入重复键值对更慢。模块作者 Mark Williams 贡献了该实现;misc/bench_omd.py 中保留了针对 OMD、FastIterOrderedMultiDict、collections.OrderedDict、内置 dict(以及可选的 werkzeug MultiDict)的基准对比脚本(该脚本依赖外部项目路径与 lithoxyl 库,仅供本地基准参考)。

增删改:add / addlist / setdefault / update / update_extend / pop 家族

方法语义源码位置
add(k, v)在键k下追加单个值,保留已有值dictutils.py
addlist(k, v)在键k下追加一个可迭代对象的全部值,保留已有值;空迭代直接返回dictutils.py
get(k, default=None)返回最近插入的值,不抛 KeyErrordictutils.py
getlist(k, default)返回该键全部值的副本(可安全修改);无 default 时返回空列表dictutils.py
setdefault(k, default=None)键不存在则写入默认值并返回它,存在则返回当前值dictutils.py
update(E, **F)合并数据并覆盖已有键的值dictutils.py
update_extend(E, **F)合并数据但不覆盖,追加到已有键下dictutils.py
pop(k, default)移除键下全部值,返回最近插入的值dictutils.py
popall(k, default)移除键下全部值,以列表形式返回dictutils.py
poplast(k, default)弹出最近插入的值;不传k时弹出最近插入的键dictutils.py
clear()清空dictutils.py
copy()/fromkeys(keys, default)浅拷贝 / 从键列表构造dictutils.py

addlist的 docstring 示例:

>>> omd = OrderedMultiDict([('a', -1)]) >>> omd.addlist('a', range(3)) >>> omd OrderedMultiDict([('a', -1), ('a', 0), ('a', 1), ('a', 2)])

注意update()与update_extend()的关键区别:前者覆盖(omd.update(omd2)后getlist('a') == [10],见 tests/test_dictutils.py 的test_update_basic),后者则在已有键下追加(测试 test_update_extend 断言len(omd1.getlist(k)) >= len(omd2.getlist(k)))。另外 OMD 支持|=就地合并运算符(__ior__,dictutils.py,测试见 test_ior)。

遍历与视图:multi 标志的妙用

iteritems/iterkeys/itervalues(以及返回列表的items/keys/values)都接受multi布尔参数(dictutils.py):

  • 默认(multi=False):每个键只出现一次,值取最近插入的那个;
  • multi=True:按插入顺序产出全部条目(含重复键)。
>>> omd = OrderedMultiDict([('a', 1), ('b', 2), ('a', 3)]) >>> omd.items() [('a', 3), ('b', 2)] >>> omd.items(multi=True) [('a', 1), ('b', 2), ('a', 3)] >>> omd.keys(multi=True) ['a', 'b', 'a']

test_kv_consistency(tests/test_dictutils.py)验证了两种模式下keys/values与items的顺序严格一致。iterkeys(multi=True)的实现是直接沿链表推进,而iterkeys()(非 multi)用yielded集合去重后按首次出现顺序产出唯一键。模块还提供viewkeys()/viewvalues()/viewitems()三套视图(dictutils.py),基于collections.abc的 KeysView/ValuesView/ItemsView。

排序:sorted 与 sortedvalues

sorted(key=None, reverse=False)(dictutils.py)返回一个按 key 函数排序的新 OMD,其 key 函数接收的是条目(key-value 二元组):

>>> omd = OrderedMultiDict(zip(range(3), range(3))) >>> omd.sorted(reverse=True) OrderedMultiDict([(2, 2), (1, 1), (0, 0)]) >>> omd = OrderedMultiDict(zip('hello', 'world')) >>> omd.sorted(key=lambda i: i[1]) # i[0] 是键,i[1] 是值 OrderedMultiDict([('o', 'd'), ('l', 'l'), ('e', 'o'), ('l', 'r'), ('h', 'w')])

sortedvalues(key=None, reverse=False)(dictutils.py)则保持键及键的顺序不变,只对每个键空间内的值排序:

>>> omd = OrderedMultiDict() >>> omd.addlist('even', [6, 2]) >>> omd.addlist('odd', [1, 5]) >>> omd.add('even', 4) >>> omd.add('odd', 3) >>> somd = omd.sortedvalues() >>> somd.getlist('even') [2, 4, 6] >>> somd.keys(multi=True) == omd.keys(multi=True) True >>> omd == somd False >>> somd OrderedMultiDict([('even', 2), ('even', 4), ('odd', 1), ('odd', 3), ('even', 6), ('odd', 5)])

如上所示:内容与键顺序被完整保留,只有值的顺序发生变化。注意sortedvalues的实现在重插时按"逆序弹出"处理(源码注释# (not reverse)),因此反向排序时传reverse=True的语义与直觉略有不同,使用时建议以 doctest 行为为准。

反转与计数:inverted 与 counts

inverted()(dictutils.py)返回一个新的 OMD,值变键、键变值,插入顺序保留、所有数据完整呈现——这正是"非破坏性"哲学的集中体现,可用来构建反向索引:

>>> omd = OMD([(0, 2), (1, 2)]) >>> omd.inverted().getlist(2) [0, 1]

反转两次得到原对象的副本:

>>> omd.inverted().inverted() OrderedMultiDict([(0, 2), (1, 2)])

counts()(dictutils.py)返回"键 → 该键下插入值数量"的映射,类似collections.Counter,但返回的是新的 OrderedMultiDict(源码注释解释了原因:Counter/OrderedDict 可能不可用,且 Counter 与 dict 都不保证顺序):

>>> omd = OMD([('a', 1), ('b', 2), ('a', 3)]) >>> omd.counts() OrderedMultiDict([('a', 2), ('b', 1)])

相等性、序列化与视图

  • 相等性(__eq__,dictutils.py):与另一个 OMD 比较时逐条(含 multi 全部条目)比较;与普通 dict 比较时按"每键最近值"比较。测试 test_eq 验证了omd == d以及dict(itemset)构造的 OMD 与原始 OMD 相等。
  • pickle 支持:__getstate__/__setstate__(dictutils.py)以 multi 条目列表作为序列化状态,test_omd_pickle(tests/test_dictutils.py)验证了空与非空 OMD 的 roundtrip,getlist('b') == [2, 3]说明多值被完整保留。
  • 构造约束:__init__限制最多 1 个位置参数,超出抛TypeError;位置参数走update_extend(不覆盖),关键字参数走update(覆盖)——这一细节来自 dictutils.py。

别名

模块末尾提供了便捷别名(dictutils.py):OMD = OrderedMultiDict、MultiDict = OrderedMultiDict,三者可互换使用。

项目内的真实应用:URL 查询参数

dictutils并非孤立模块。在 boltons 自己的 urlutils 中,URL.query_params是QueryParamDict的实例,而QueryParamDict正是OrderedMultiDict 的子类型,用于承载问号之后的文本键值对(还提供别名qp):

>>> url = URL('http://boltons.readthedocs.io/en/latest/?utm_source=docs&sphinx=ok') >>> url.qp.keys() ['utm_source', 'sphinx']

这也解释了为什么 OMD 需要"一个键多个值":URL 查询参数天然允许?a=1&a=2这样的重复键,且顺序有语义。相关实现可见 boltons/urlutils.py(其中保留了 OMD 的嵌入式副本用于 QueryParamDict)。在URL.__init__中,query_params参数接受 "OMD、dict 或 (key, value) 列表"(boltons/urlutils.py)。

OneToOne:自动维护反向映射的一对一字典

OneToOne(dictutils.py)实现一对一映射:除继承并完全像内置 dict 一样工作外,所有值会被自动加入反向映射,以inv属性暴露,键与值的命名空间相互独立。

>>> from boltons.dictutils import OneToOne >>> oto = OneToOne({'a': 1, 'b': 2}) >>> print(oto['a']) 1 >>> print(oto.inv[1]) a >>> len(oto) 2

双向覆盖同样生效:

>>> oto.inv[1] = 'c' # 反向覆盖 >>> print(oto.get('a')) None # 原键 'a' 被移除 >>> len(oto) 2

源码要点:

  • 通过_OTO_INV_MARKER哨兵实现正向/反向字典的内部互建,避免递归构造(dictutils.py);
  • __setitem__先hash(val)保证值可作为键,再同步维护两个方向(dictutils.py);
  • 值不唯一时默认以"后写覆盖"收敛为合法的一对一映射(测试 test_one_to_one 验证了OneToOne({'a': 0, 'b': 0})收敛为len(oto) == len(oto.inv) == 1);
  • OneToOne.unique(*a, **kw)(dictutils.py)是严格构造器:输入值一旦重叠立即抛ValueError(expected unique values, got multiple keys for the following values: ...),对 dict 与关键字参数混合输入同样生效:
>>> OneToOne.unique({'a': 1, 'b': 1}) Traceback (most recent call last): ... ValueError: expected unique values, got multiple keys for the following values: ... >>> a_dict = {'a': 2} >>> OneToOne.unique(a_dict, b=2) Traceback (most recent call last): ... ValueError: ...

pop/popitem/setdefault/clear/update均被重写以保持双向一致;update会先hash所有输入值再逐项写入(dictutils.py)。

ManyToMany:多对多关系与有向图

ManyToMany(dictutils.py)是类字典实体,表示两组对象之间的多对多关系:行为类似"dict-of-tuples",并带有始终保持同步的.inv(反方向同样是 dict-of-tuples)。它还可以当作由可哈希 Python 对象构成的有向图使用。

>>> from boltons.dictutils import ManyToMany >>> m2m = ManyToMany() >>> m2m.add(1, 'a') >>> m2m.add(1, 'b') >>> m2m[1] # __getitem__ 返回 frozenset 视图 frozenset({'a', 'b'}) >>> m2m.inv['a'] frozenset({1}) >>> m2m.get(3) # 缺省键返回空 frozenset,不抛异常 frozenset()

常用操作:

  • add(key, val)/remove(key, val):双向同步增删(dictutils.py);
  • __setitem__(key, vals):整组替换关联集合,内部计算差集增量增删(dictutils.py);
  • replace(key, newkey):把 key 的所有关联整体迁移到 newkey 下(dictutils.py);
  • update(iterable):接受本类型实例、有keys的对象或(key, val)可迭代(dictutils.py);
  • 支持len、in、迭代、==与repr。

测试 test_many_to_many 验证了双向删除的级联效果(del m2m.inv['a']后m2m[1]只剩'b')、ManyToMany(['ab', 'cd']) == ManyToMany(['ba', 'dc']).inv的反向对称性以及replace行为。

subdict:按 keep/drop 过滤字典

subdict(d, keep=None, drop=None)(dictutils.py)计算字典的"子字典"——subdict 之于 dict,正如 subset 之于 set:若 A 是 B 的 subdict,则 A 的所有键都在 B 中出现。它返回一个新字典,移除drop中的键、保留keep中的键(仅当原字典中存在)。keep默认全部键,drop默认空,因此两个参数都不传时等价于dict():

>>> from boltons.dictutils import subdict >>> from pprint import pprint as pp >>> pp(subdict({'a': 1, 'b': 2})) {'a': 1, 'b': 2} >>> subdict({'a': 1, 'b': 2, 'c': 3}, drop=['b', 'c']) {'a': 1} >>> pp(subdict({'a': 1, 'b': 2, 'c': 3}, keep=['a', 'c'])) {'a': 1, 'c': 3}

实现用set(keep) - set(drop)求保留键集合,并以type(d)构造返回值(dictutils.py),因此尽量保持传入字典的类型——测试 test_subdict_keep_type 断言subdict(omd)的类型仍是 OMD。drop与keep也可同时使用(如test_subdict中的subdict(cap_map, drop=['a']),tests/test_dictutils.py)。

FrozenDict:可哈希的不可变字典

FrozenDict(dictutils.py)是不可变的 dict 子类型,可哈希,能直接作为 dict 的键或 set 的元素——正如frozenset之于set,FrozenDict 之于dict。曾有提议将其引入标准库,但被拒绝(PEP 416,详见模块 docstring dictutils.py)。

由于 FrozenDict 是 dict 子类型,它自动适用于任何 dict 可用的场合,包括 JSON 序列化:

>>> from boltons.dictutils import FrozenDict >>> fd = FrozenDict({'a': 'A', 'b': 'B'}) >>> fd['a'] 'A' >>> hash(fd) # 可哈希 >>> {'fd': fd}['fd'] is fd # 可作为 dict 键 True

不可变性:所有修改型方法(__setitem__、__delitem__、update、setdefault、pop、popitem、clear、|=)统一抛TypeError: FrozenDict object is immutable(dictutils.py)。测试 test_frozendict 逐项验证了这些异常。

实用特性:

  • updated(*a, **kw)(dictutils.py):复制并追加条目(覆盖已有键),返回新FrozenDict——这是不可变映射的标准"修改"方式:
    >>> fkfd = FrozenDict.fromkeys([2, 4, 6], value=0) >>> sorted(fkfd.updated({8: 0}).keys()) [2, 4, 6, 8]
  • FrozenDict.fromkeys(keys, value=None)(dictutils.py);
  • __hash__基于hash(frozenset(self.items()))并缓存在_hash槽位;若值不可哈希,缓存的FrozenHashError(TypeError子类,dictutils.py)会被原样重抛,且同一异常对象被缓存复用——测试断言两次{unfd: 'val'}抛出的是同一异常实例(tests/test_dictutils.py);
  • __copy__返回自身(不可变类型无需复制,与 tuple 行为一致),且实现了 pickle 支持(__reduce_ex__)。

测试与可靠性保障

dictutils的每个核心行为都有对应测试,集中在 tests/test_dictutils.py:

  • 正确性:test_multi_correctness(第 113 行)用 100 个键、5 倍冗余的构造数据验证两种multi模式下迭代值严格升序(即插入顺序完整保留);
  • 一致性:test_kv_consistency(第 129 行)验证 keys/values/items 顺序互洽;
  • 兼容性:test_types(第 105 行)断言 OMD 既是dict又是collections.abc.MutableMapping的实例;
  • 边界:test_addlist(第 222 行)验证addlist('a', [])不产生任何条目;test_setdefault(第 282 行)验证返回对象身份(x is empty_list);
  • Python 3.9+ 特性:test_frozendict_ior(第 479 行)验证 PEP 584 的|=对 FrozenDict 抛TypeError。

运行方式:pytest tests/test_dictutils.py(依赖见 requirements-test.txt)。

小结:如何选择 dictutils 中的类型

类型/函数核心特性典型场景
OrderedMultiDict/OMD/MultiDict有序 + 一键多值 + 非破坏性URL 查询参数、反向索引、多源数据非破坏性叠加(见 docs/index.rst 的项目定位)
FastIterOrderedMultiDict跳表实现,按键迭代更快、常量内存读多写少、需要频繁全量遍历的场景
OneToOne双向映射自动同步(inv属性)用户↔ID、缩写↔全称等唯一映射,正反查询
ManyToMany多对多关系,双向inv同步标签↔文章、权限↔角色、有向图建模
subdict(d, keep, drop)按 keep/drop 过滤,尽量保持原类型请求参数白名单、字段裁剪
FrozenDict不可变、可哈希、dict 子类型配置常量、可哈希缓存键、安全共享只读映射
# 一行导入全部所需工具 from boltons.dictutils import (OrderedMultiDict, OMD, MultiDict, OneToOne, ManyToMany, subdict, FrozenDict)

完整 API 文档入口位于 docs/dictutils.rst,模块变更历史可查阅 CHANGELOG.md 中dictutils相关条目(如 OMD pickle 修复、ior支持、sorted()保留全部条目、OneToOne 空可迭代 update 修复等)。

  • 开发工具

【免费下载链接】boltons

🔩 Like builtins, but boltons. 250+ constructs, recipes, and snippets which extend (and rely on nothing but) the Python standard library. Nothing like Michael Bolton.

项目地址:https://gitcode.com/gh_mirrors/bo/boltons
点击查看免费下载

相关推荐

上一篇:Moya 单元测试与接口 Mock 实战:深入 sampleData、stubClosure 与 EndpointSampleResponse
下一篇:Pandoc 嵌套列表转换深度解析:HTML 到 Markdown 的实现与命令行测试(test/command/8150.md)

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询