簡體   English   中英

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

[英]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

Counterdefaultdict(int)明顯慢。 我發現性能下降通常比defaultdict(int)慢約2 defaultdict(int)

另外,顯然Counter只能與defaultdict int參數相媲美,而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

關於賞金評論的注意事項

@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)是迄今為止最快的。 DictWithDefaultCythondefaultdict是相等的( 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它采用了一個定點trycatch方法。

你的賞金信息說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)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM