collections.ChainMap的目的是什么?

95
在Python 3.3中,ChainMap类被添加到collections模块中:

提供了一个ChainMap类,用于快速链接多个映射,以便将它们视为单个单位。它通常比创建新字典并运行多个update()调用要快得多。

示例:

>>> from collections import ChainMap
>>> x = {'a': 1, 'b': 2}
>>> y = {'b': 10, 'c': 11}
>>> z = ChainMap(y, x)
>>> for k, v in z.items():
        print(k, v)
a 1
c 11
b 10

这个想法源于 这个问题 并由 这个人 公开发表(无需创建 PEP)。

据我所知,它是拥有额外的字典并通过 update() 来维护它的一种替代方案。

问题是:

  • ChainMap 能够解决哪些使用案例?
  • 是否有任何关于 ChainMap 的现实世界的示例?
  • 它在第三方库中被用于切换到 python3 吗?

奖励问题:是否有方法在 Python2.x 上使用它?


我在Raymond Hettinger的Transforming Code into Beautiful, Idiomatic Python PyCon演讲中听说过它,我想将其添加到我的工具包中,但我不太理解何时应该使用它。


5
实际使用案例:Web框架中的GET和POST参数映射,提供对两个独立字典的综合视图。 - Martijn Pieters
2
关于在2.x中使用它,源代码看起来可能会正常工作,尽管我还没有尝试过。 - mhlester
20
值得一提的是,Python2.7中已经有了一个前置版本:from ConfigParser import _ChainMap as ChainMap - Raymond Hettinger
4
@RaymondHettinger 感谢您美好而有用的评论和ChainMap本身。您可以将这一系列评论转化为一个很好的回答 :) - alecxe
6
如果你想按照Raymond的建议在Python 2.7中导入,实际上应该使用"_Chainmap"(注意m的大小写)。 - Rob Dennis
显示剩余3条评论
4个回答

88

我喜欢@b4hand的例子,事实上在过去我曾经使用类似ChainMap结构(但不是ChainMap本身)来实现他提到的两个目的: 多层配置覆盖和变量栈/范围仿真。

我想指出ChainMap相比于使用字典更新循环时的另外两个动机/优势/区别:

  1. 更多信息:由于ChainMap结构是“分层”的,因此它支持回答这样的问题:我是否得到了“默认”值或被覆盖的值?什么是原始(“默认”)值?值在哪个级别被覆盖(借用@b4hand的配置示例:用户配置或命令行覆盖)?使用简单的字典,回答这些问题所需的信息已经丢失。

  2. 速度平衡:假设您有N层,每层最多有M个键,构建一个ChainMap需要O(N)时间,并且每次查找最坏情况下需要O(N)[*],而使用更新循环构建字典需要O(NM)时间,并且每次查找需要O(1)时间。这意味着,如果您经常构建并且每次只执行少量查找,或者M很大,则ChainMap的惰性构建方法会对您有利。

[*] (2)中的分析假定字典访问是O(1),而实际上平均情况下是O(1),最坏情况下是O(M)。更多详情请参见此处


9
这是一个合理的比较。如果能与其他技术进行类比会更好。例如,操作系统命令行有一个“路径”的概念,它实质上是一系列目录查找直到找到匹配项。在Python中,这可以用ChainMap来模拟。 - Raymond Hettinger

48

我认为可以使用 ChainMap 来创建一个配置对象,在其中有多个配置范围,例如命令行选项、用户配置文件和系统配置文件。由于查找是按构造函数参数中的顺序排序的,因此您可以覆盖较低范围的设置。我个人没有使用过或看到过 ChainMap 的使用,但这并不奇怪,因为它是标准库中相对较新的添加。

如果您试图自己实现词法作用域,那么使用 ChainMap 也可能很有用,以模拟推入和弹出变量绑定的堆栈帧。

Python 标准库文档中关于 ChainMap 的说明 给出了多个示例,并提供了指向第三方库类似实现的链接。具体而言,它提到了 Django 的 Context 类 和 Enthought's 的 MultiContext 类


7
这是一个不错的回答,涵盖了备选实现,并提供了多个链接到文档中提到的链接名称空间的示例。 - Raymond Hettinger

7
我来试着解释一下:
Chainmap 看起来是一种非常精巧的抽象方法,它适用于一种非常特殊的问题。我提出这个使用案例。
如果你有:
1. 多个映射(例如字典); 2. 在这些映射中某些键存在重复(同一键可以在多个映射中出现,但不是所有键都会在所有映射中出现); 3. 消费应用程序希望访问“最高优先级”映射中键的值,对于任何给定的键,所有映射之间存在总排序(也就是说,映射可能具有相等的优先级,但只有在已知这些映射中没有键的重复的情况下才行)(在 Python 应用程序中,包可以位于同一个目录中(相同的优先级),但必须具有不同的名称,因此,根据定义,该目录中的符号名称不能重复。); 4. 消费应用程序不需要更改键的值; 5. 同时,这些映射必须保持其独立身份,并且可以被外部力量异步更改; 6. 并且这些映射足够大,足够昂贵或者在应用程序访问之间经常更改,以至于每次应用程序需要计算投影(3)的成本都是一个显著的性能问题……
那么,你可能需要考虑使用 Chainmap 来创建对映射集合的视图。
但这些都是事后的证明。Python 的开发者们遇到了问题,在他们的代码环境中想出了一个好的解决方案,然后做了一些额外的工作来抽象他们的解决方案,以便我们可以选择使用它。向他们致敬。但是否适合你的问题取决于你自己决定。

Chainmap在所有映射中都出现键时非常有用,例如如果您想要推送和弹出映射的版本。 - Marcin
很好,但是如果你的所有字典都有相同的键,那么你可能可以更快地创建一个新的单一字典并更新值。 - BobHy
1
是的,这样会更快,但它不支持弹出/推入功能。 - Marcin

5
为了回答你的问题:
奖励问题:有没有办法在Python2.x上使用它?
from ConfigParser import _Chainmap as ChainMap

请注意,这不是一个真正的ChainMap,它继承自DictMixin并且只定义了:

__init__(self, *maps)
__getitem__(self, key)
keys(self)

# And from DictMixin:
__iter__(self)
has_key(self, key)
__contains__(self, key)
iteritems(self)
iterkeys(self)
itervalues(self)
values(self)
items(self)
clear(self)
setdefault(self, key, default=None)
pop(self, key, *args)
popitem(self)
update(self, other=None, **kwargs)
get(self, key, default=None)
__repr__(self)
__cmp__(self, other)
__len__(self)

它的实现似乎也不是特别高效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接