当在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
可以采用list
,set
等
有没有办法有效地实施上述行为;例如,通过继承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评论。
答案 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)
请注意,它使用对数日志比例来更好地了解差异:
对于长时间的迭代,Counter(iterable)
是迄今为止最快的。 DictWithDefaultCython
和defaultdict
相等(大多数情况下DictWithDefault
稍微快一点,即使此处不可见),然后是DictWithDefault
然后{{ 1}}使用手动Counter
- 循环。有趣的是for
是最快和最慢的。
我掩饰的事实是,它与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
,当您不想存储默认值时,不要使用[]
。您可以使用in
或get
:
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)