禁止在collections.defaultdict中添加键添加

时间:2018-04-11 15:03:37

标签: python oop dictionary collections defaultdict

当在defaultdict对象中查询缺失的密钥时,密钥会自动添加到字典中:

from collections import defaultdict

d = defaultdict(int)
res = d[5]

print(d)
# defaultdict(<class 'int'>, {5: 0})
# we want this dictionary to remain empty

但是,我们通常只想在显式或隐式分配密钥时添加密钥:

d[8] = 1  # we want this key added
d[3] += 1 # we want this key added

一个用例是简单计数,以避免collections.Counter的更高开销,但这个功能也可能是一般的。

反例 [原谅双关语]

这是我想要的功能:

from collections import Counter
c = Counter()
res = c[5]  # 0
print(c)  # Counter()

c[8] = 1  # key added successfully
c[3] += 1 # key added successfully

Counter明显慢于defaultdict(int)。我发现性能影响通常比defaultdict(int)慢约2倍。

此外,显然Counter仅与int中的defaultdict参数相当,而defaultdict可以采用listset

有没有办法有效地实施上述行为;例如,通过继承defaultdict

基准测试示例

%timeit DwD(lst)           # 72 ms
%timeit dd(lst)            # 44 ms
%timeit counter_func(lst)  # 98 ms
%timeit af(lst)            # 72 ms

测试代码:

import numpy as np
from collections import defaultdict, Counter, UserDict

class DefaultDict(defaultdict):
    def get_and_forget(self, key):
        _sentinel = object()
        value = self.get(key, _sentinel)

        if value is _sentinel:
            return self.default_factory()
        return value

class DictWithDefaults(dict):
    __slots__ = ['_factory']  # avoid using extra memory

    def __init__(self, factory, *args, **kwargs):
        self._factory = factory
        super().__init__(*args, **kwargs)

    def __missing__(self, key):
        return self._factory()

lst = np.random.randint(0, 10, 100000)

def DwD(lst):
    d = DictWithDefaults(int)
    for i in lst:
        d[i] += 1
    return d

def dd(lst):
    d = defaultdict(int)
    for i in lst:
        d[i] += 1
    return d

def counter_func(lst):
    d = Counter()
    for i in lst:
        d[i] += 1
    return d

def af(lst):
    d = DefaultDict(int)
    for i in lst:
        d[i] += 1
    return d

关于赏金评论的说明

自从Bounty被提供以来,

@Aran-Fey's solution已经更新,所以请忽略Bounty评论。

3 个答案:

答案 0 :(得分:7)

而不是弄乱collections.defaultdict使其成为我们想要的东西,实现我们自己似乎更容易:

class DefaultDict(dict):
    def __init__(self, default_factory, **kwargs):
        super().__init__(**kwargs)

        self.default_factory = default_factory

    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            return self.default_factory()

这可以按您的方式工作:

d = DefaultDict(int)

res = d[5]
d[8] = 1 
d[3] += 1

print(d)  # {8: 1, 3: 1}

但是,对于可变类型,它可能会出现意外行为:

d = DefaultDict(list)
d[5].append('foobar')

print(d)  # output: {}

这可能是defaultdict在访问不存在的密钥时记住该值的原因。

另一种选择是扩展defaultdict并添加一个新方法来查找值而不记住它:

from collections import defaultdict

class DefaultDict(defaultdict):
    def get_and_forget(self, key):
        return self.get(key, self.default_factory())

请注意,get_and_forget方法每次调用default_factory(),无论密钥是否已存在于dict中。如果这是不合需要的,您可以使用sentinel值来实现它:

class DefaultDict(defaultdict):
    def get_and_forget(self, key):
        _sentinel = object()
        value = self.get(key, _sentinel)

        if value is _sentinel:
            return self.default_factory()
        return value

这对可变类型有更好的支持,因为它允许你选择是否应该将值添加到dict中。

答案 1 :(得分:5)

如果您只想要在访问不存在的密钥时返回默认值dict,那么您可以简单地将dict子类化并实现__missing__

  

object.__missing__(self, key)

     

dict.__getitem__()调用,当self[key]不在字典中时,为dict子类实现key

看起来像这样:

class DictWithDefaults(dict):
    # not necessary, just a memory optimization
    __slots__ = ['_factory']  

    def __init__(self, factory, *args, **kwargs):
        self._factory = factory
        super().__init__(*args, **kwargs)

    def __missing__(self, key):
        return self._factory()

在这种情况下,我使用类似defaultdict的方法,因此您必须传入一个factory,它应该在调用时提供默认值:

>>> dwd = DictWithDefaults(int)
>>> dwd[0]  # key does not exist
0 
>>> dwd     # key still doesn't exist
{}
>>> dwd[0] = 10
>>> dwd
{0: 10}

当您进行分配(显式或隐式)时,该值将被添加到字典中:

>>> dwd = DictWithDefaults(int)
>>> dwd[0] += 1
>>> dwd
{0: 1}

>>> dwd = DictWithDefaults(list)
>>> dwd[0] += [1]
>>> dwd
{0: [1]}

您想知道collections.Counter是如何做到这一点的,从CPython 3.6.5开始,它也使用了__missing__

class Counter(dict):
    ...

    def __missing__(self, key):
        'The count of elements not in the Counter is zero.'
        # Needed so that self[missing_item] does not raise KeyError
        return 0

    ...

更好的表现?!

您提到速度是值得关注的,因此您可以将该类设置为C扩展类(假设您使用CPython),例如使用Cython(我使用Jupyter magic命令创建扩展类):

%load_ext cython

%%cython

cdef class DictWithDefaultsCython(dict):
    cdef object _factory

    def __init__(self, factory, *args, **kwargs):
        self._factory = factory
        super().__init__(*args, **kwargs)

    def __missing__(self, key):
        return self._factory()

基准

根据您的基准:

from collections import Counter, defaultdict

def d_py(lst):
    d = DictWithDefaults(int)
    for i in lst:
        d[i] += 1
    return d

def d_cy(lst):
    d = DictWithDefaultsCython(int)
    for i in lst:
        d[i] += 1
    return d

def d_dd(lst):
    d = defaultdict(int)
    for i in lst:
        d[i] += 1
    return d

鉴于这只是在计算,仅仅使用Counter初始化程序而不包括基准将是一种(不可原谅的)疏忽。

我最近编写了一个小型基准测试工具,我认为这可能会派上用场(但你也可以使用%timeit来完成):

from simple_benchmark import benchmark
import random

sizes = [2**i for i in range(2, 20)]
unique_lists = {i: list(range(i)) for i in sizes}
identical_lists = {i: [0]*i for i in sizes}
mixed = {i: [random.randint(0, i // 2) for j in range(i)]  for i in sizes}

functions = [d_py, d_cy, d_dd, d_c, Counter]

b_unique = benchmark(functions, unique_lists, 'list size')
b_identical = benchmark(functions, identical_lists, 'list size')
b_mixed = benchmark(functions, mixed, 'list size')

结果如下:

import matplotlib.pyplot as plt

f, (ax1, ax2, ax3) = plt.subplots(1, 3, sharey=True)
ax1.set_title('unique elements')
ax2.set_title('identical elements')
ax3.set_title('mixed elements')
b_unique.plot(ax=ax1)
b_identical.plot(ax=ax2)
b_mixed.plot(ax=ax3)

请注意,它使用对数日志比例来更好地了解差异:

enter image description here

对于长时间的迭代,Counter(iterable)是迄今为止最快的。 DictWithDefaultCythondefaultdict相等(大多数情况下DictWithDefault稍微快一点,即使此处不可见),然后是DictWithDefault然后{{ 1}}使用手动Counter - 循环。有趣的是for是最快和最慢的。

隐式添加返回值,如果它是modifie

我掩饰的事实是,它与Counter有很大不同,因为期望&#34;只是返回默认值,不要保存它&#34;可变类型:

defaultdict

这意味着当您希望修改后的值在字典中可见时,实际上需要设置元素。

然而,这有点引起了我的兴趣,所以我想分享一下如何使这项工作(如果需要)。但它只是一个快速测试,仅适用于使用代理的>>> from collections import defaultdict >>> dd = defaultdict(list) >>> dd[0].append(10) >>> dd defaultdict(list, {0: [10]}) >>> dwd = DictWithDefaults(list) >>> dwd[0].append(10) >>> dwd {} 次呼叫。请不要在生产代码中使用它(从我的观点来看,这只具有娱乐价值):

append

这是一个返回代理对象(而不是真正的默认值)的字典,它会重载一个方法,如果调用该方法,则将返回值添加到字典中。并且&#34;工作&#34;:

from wrapt import ObjectProxy

class DictWithDefaultsFunky(dict):
    __slots__ = ['_factory']  # avoid using extra memory

    def __init__(self, factory, *args, **kwargs):
        self._factory = factory
        super().__init__(*args, **kwargs)

    def __missing__(self, key):
        ret = self._factory()
        dict_ = self

        class AppendTrigger(ObjectProxy):
            def append(self, val):
                self.__wrapped__.append(val)
                dict_[key] = ret

        return AppendTrigger(ret)

但它确实有一些陷阱(可以解决,但它只是一个概念验证,所以我不会在这里尝试):

>>> d = DictWithDefaultsFunky(list)
>>> a = d[10]
>>> d
[]

>>> a.append(1)
>>> d
{10: [1]}

如果您真的想要这样的东西,您可能需要实现一个真正跟踪值内变化的类(而不仅仅是>>> d = DictWithDefaultsFunky(list) >>> a = d[10] >>> b = d[10] >>> d {} >>> a.append(1) >>> d {10: [1]} >>> b.append(10) >>> d # oups, that overwrote the previous stored value ... {10: [10]} 个调用)。

如果您想避免隐式分配

如果您不喜欢append或类似操作将值添加到字典中(与之前尝试以非常隐式方式添加值的示例相反),那么您可能应该将其实现为方法而不是特殊方法。

例如:

+=

这类似于Aran-Feys回答的行为,但是使用class SpecialDict(dict): __slots__ = ['_factory'] def __init__(self, factory, *args, **kwargs): self._factory = factory def get_or_default_from_factory(self, key): try: return self[key] except KeyError: return self._factory() >>> sd = SpecialDict(int) >>> sd.get_or_default_from_factory(0) 0 >>> sd {} >>> sd[0] = sd.get_or_default_from_factory(0) + 1 >>> sd {0: 1} get方法而不是带有哨兵的try

答案 2 :(得分:2)

你的赏金信息说Aran-Fey的答案“不适用于可变类型”。 (对于未来的读者,赏金信息是“当前答案是好的,但它不适用于可变类型。如果可以调整现有答案,或提出其他选项解决方案,为了这个目的,这将是理想的。 “)

问题是,它确实适用于可变类型:

>>> d = DefaultDict(list)
>>> d[0] += [1]
>>> d[0]
[1]
>>> d[1]
[]
>>> 1 in d
False

无法工作的内容类似于d[1].append(2)

>>> d[1].append(2)
>>> d[1]
[]

那是因为这不涉及对dict的商店操作。唯一涉及的字典操作是项目检索。

dict对象在d[1]d[1].append(2)中看到的没有区别。 dict不参与append操作。如果没有令人讨厌的,脆弱的堆栈检查或类似的东西,dict就无法仅为d[1].append(2)存储列表。

所以那是绝望的。你应该怎么做?

好吧,一个选项是使用常规collections.defaultdict,当您不想存储默认值时,不要使用[]。您可以使用inget

if key in d:
    value = d[key]
else:
    ...

value = d.get(key, sentinel)

或者,您可以在不需要时关闭默认工厂。当您有单独的“构建”和“读取”阶段时,这通常是合理的,并且您在读取阶段不需要默认工厂:

d = collections.defaultdict(list)
for thing in whatever:
    d[thing].append(other_thing)
# turn off default factory
d.default_factory = None
use(d)