将`defaultdict`作为常规`dict`公开暴露

40
我正在使用defaultdict(set)来填充一个非常大的数据结构中的内部映射。填充完成后,整个结构(包括映射)都会暴露给客户端代码。此时,我不希望任何人修改映射。即使是有意无意地引用不存在的元素,正常字典会引发KeyError异常,但由于映射是defaultdict,它只会在该键处创建一个新元素(一个空集)。这很难捕捉,因为一切都是在默默无闻中发生。但我需要确保这不会发生(实际上,语义并没有被破坏,但映射会增长到一个巨大的大小)。
我该怎么办?我可以看到以下选择:
1. 找到所有当前和未来客户端代码中执行映射查找的实例,并将其转换为mapping.get(k, {})。这太可怕了。 2. 在数据结构完全初始化后“冻结”defaultdict,通过将其转换为dict来实现。(我知道它并没有真正被冻结,但我相信客户端代码实际上不会编写mapping[k] = v。)不优雅,性能损失很大。 3. 将defaultdict包装成一个dict接口。有什么优雅的方法可以做到这一点吗?但我担心性能损失会很大(在紧密循环中广泛使用此查找)。 4. 子类化defaultdict并添加一个方法,该方法“关闭”所有defaultdict功能,使其表现得好像它是一个常规的dict。这是上述第3种方法的变体,但我不确定它是否更快。而且我不知道是否可以在不依赖于实现细节的情况下完成。
  • 使用普通的dict数据结构,在重写所有代码之前,首先检查元素是否在字典中,如果不在,则将其添加。这样做不够好。


  • 1
    “重写”只需使用dict.setdefault方法即可... 没什么大不了的。 - JBernardo
    我认为你只需要在defaultdict上调用dict来将其转换为字典。 - inspectorG4dget
    @Pyson:啊,你说得对,这很有道理。但这不是支持永远不使用defaultdict的论点吗?(我并不反对,只是想理解逻辑。) - max
    2
    @inspectorG4dget 数据结构的大小超过1GB,因此复制所有数据(如果调用dict将会发生)太昂贵了。 - max
    @Pyson:为什么?dict.setdefault是用C实现的,它和defaultdict.__getitem__做的事情完全一样。它不应该同样快吗? - max
    显示剩余4条评论
    3个回答

    65

    defaultdict 的文档中关于 default_factory 的说明如下:

    如果 default_factory 属性为 None,则使用键作为参数引发 KeyError 异常。

    那么,如果您将 defaultdict 的 default_factory 设置为 None,会出现什么情况呢?例如:

    >>> d = defaultdict(int)
    >>> d['a'] += 1
    >>> d
    defaultdict(<type 'int'>, {'a': 1})
    >>> d.default_factory = None
    >>> d['b'] += 2
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    KeyError: 'b'
    >>> 
    

    不确定这是否是最佳方法,但似乎有效。


    5
    谁知道我提出的解决方案已经作为defaultdict的特性实现了呢?太好了。 (+1) - mgilson
    哇,这太完美了。我希望可以安全地更改现有的defaultdict对象的default_factory(我不知道为什么不能)。 - max
    3
    @max -- 文档明确说明default_factory是一个可写属性,因此它应该是安全的。 - mgilson
    @max:使用源代码:defdictobjectdefdict_members(名称、类型、偏移量、标志、文档;flags==0表示它是可写的),[defdict_missing](http://hg.python.org/cpython/file/3d0686d90f55/Modules/_collectionsmodule.c#l1262)。 - Eryk Sun

    4

    一旦您完成了填充 defaultdict 的工作,您可以直接从中创建一个普通字典:

    my_dict = dict(my_default_dict)
    

    如果默认的 dict 是一个递归的默认字典,请参考 this answer 中的递归解决方案。

    0
    你可以创建一个类来持有对字典的引用,并防止使用setitem()方法。
    from collections import Mapping
    
    class MyDict(Mapping):
        def __init__(self, d):
            self.d = d;
    
        def __getitem__(self, k):
            return self.d[k]
    
        def __iter__(self):
            return self.__iter__()
    
        def __setitem__(self, k, v):
            if k not in self.d.keys():
                raise KeyError
            else:
                self.d[k] = v
    

    它使用纯Python进行关键方法,速度会不会非常慢? - max
    对于getitem方法?不确定与defaultdict相比的性能开销。 - pyrospade
    无论如何,我认为Neal的解决方案是最适合你的问题的。 - pyrospade

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