[英]Suppress key addition in 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 defaultdict(int)
。
另外,顯然Counter
只能與defaultdict
int
參數相媲美,而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
關於賞金評論的注意事項 :
@Aran-Fey的解決方案自Bounty發布以來已經更新,所以請忽略Bounty的評論。
而不是亂搞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())
請注意,無論密鑰是否已存在於dict中, get_and_forget
方法每次都會調用default_factory()
。 如果這是不合需要的,您可以使用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中。
如果您只想要一個在訪問不存在的鍵時返回默認值的dict
,那么您可以簡單地__missing__
dict
並實現__missing__
:
object.__missing__(self, key)
通過被稱為
dict.__getitem__()
來實現self[key]
對於字典亞類時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
,然后是Counter
with the manual for
-loop。 有趣的是, Counter
是最快和最慢的。
我掩蓋的事實是它與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
調用。 請不要在生產代碼中使用它(從我的觀點來看,這只具有娛樂價值):
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
調用)。
如果您不喜歡+=
或類似操作將值添加到字典(與前一個甚至嘗試以非常隱式方式添加值的示例相反)的事實,那么您可能應該將其實現為方法而不是作為特殊方法。
例如:
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}
這是類似的阿蘭- Feys答案的行為,但不是get
它采用了一個定點try
和catch
方法。
你的賞金信息說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)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.