Collections.defaultdict与普通字典的区别

807

我已经阅读了 Python 文档中的示例,但仍然无法理解这个方法的含义。有人可以帮忙吗?以下是 Python 文档中的两个示例:

>>> from collections import defaultdict

>>> s = 'mississippi'
>>> d = defaultdict(int)
>>> for k in s:
...     d[k] += 1
...
>>> d.items()
dict_items([('m', 1), ('i', 4), ('s', 4), ('p', 2)])

>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> for k, v in s:
...     d[k].append(v)
...
>>> d.items()
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]

参数intlist是用于什么的?


41
顺便提一句,根据你的使用情况,请别忘了在填充完defaultdict后将其设置为只读模式并冻结它,方法是将default_factory = None。参考这个问题 - Asclepius
1
参见:https://dev59.com/x2Qm5IYBdhLWcg3w8iuD - dreftymac
16个回答

866

通常,如果你试图使用当前字典中不存在的键来获取项目,Python字典会抛出一个KeyError异常。相比之下,defaultdict会简单地创建任何您尝试访问的项(前提是它们尚不存在)。为了创建这样一个“默认”项,它调用您传递给构造函数的函数对象(更准确地说,它是一个任意的“可调用”对象,包括函数和类型对象)。在第一个示例中,使用int()创建默认项,将返回整数对象0。在第二个示例中,使用list()创建默认项,将返回一个新的空列表对象。


19
与使用 d.get(key, default_val) 相比,它在功能上有何不同? - Ambareesh
100
d.get(key, default)不会修改你的字典,它只会返回默认值并保持字典不变。另一方面,defaultdict如果字典中没有该键,则会将该键插入到字典中。这是一个很大的区别,请参考问题中的示例以了解原因。 - Sven Marnach
1
我们如何知道每种类型的默认值是什么?int() 的默认值为0,list() 的默认值为[],这很直观,但也可能存在更复杂或自定义的类型。 - Sean
5
@Sean:defaultdict 会调用你传入的构造函数。如果你传入类型 T,则值将使用 T() 进行构造。并不是所有类型都可以在不传递任何参数的情况下进行构建。如果您想构造这样的类型,您需要一个包装函数或类似 functools.partial(T, arg1, arg2) 的东西。 - Sven Marnach
6
甚至更简单的方式是:一个 lambda 表达式。defaultdict(lambda : T(arg1, arg2)) - Mees de Vries

307

defaultdict的意思是,如果在字典中找不到键,则不会引发KeyError,而是创建一个新条目。该新条目的类型由defaultdict的参数指定。

例如:

somedict = {}
print(somedict[3]) # KeyError

someddict = defaultdict(int)
print(someddict[3]) # print int(), thus 0

13
这个新键值对的类型由 defaultdict 的参数决定。需要注意的是,这里的参数可以是任何可调用对象,不仅限于类型函数。例如,如果 foo 是一个返回“bar”的函数,那么 foo 可以作为 default dict 的参数,如果访问了不存在的键,则会将其值设置为“bar”。 - lf215
23
如果您只想返回“bar”:somedict = defaultdict(lambda:"bar")。 注:这是Python代码,使用了一个名为defaultdict的数据结构,意思是如果字典(somedict)中没有某个键,则默认返回一个lambda函数的结果,即字符串“bar”。 - Michael Scott Asato Cuthbert
第四行返回整数 0,如果它是 someddict = defaultdict(list),则返回 [ ]。0 是默认整数吗?还是 [ ] 是默认列表? - Gathide
在CPython中,从-5到256的所有值都是缓存的单例,但这是特定于实现的行为 - 在两种情况下,每次使用int()或list()都会创建一个新实例。这样,d[k].append(v)就可以工作,而不必用指向相同列表的引用填充字典,这将使defaultdict几乎无用。如果这是行为,则defaultdict将以值而不是lambda作为参数。 (对糟糕的解释感到抱歉!) - wizzwizz4

142

defaultdict

标准字典包括setdefault()方法来检索值并在值不存在时设置默认值。相比之下,defaultdict允许调用者在容器初始化时事先指定默认值(要返回的值)。

Doug HellmannThe Python Standard Library by Example中定义。

如何使用defaultdict

导入defaultdict

>>> from collections import defaultdict

初始化defaultdict

通过将callable作为第一个参数(必需)传递来进行初始化

>>> d_int = defaultdict(int)
>>> d_list = defaultdict(list)
>>> def foo():
...     return 'default value'
... 
>>> d_foo = defaultdict(foo)
>>> d_int
defaultdict(<type 'int'>, {})
>>> d_list
defaultdict(<type 'list'>, {})
>>> d_foo
defaultdict(<function foo at 0x7f34a0a69578>, {})

**kwargs作为其第二个参数(可选)

>>> d_int = defaultdict(int, a=10, b=12, c=13)
>>> d_int
defaultdict(<type 'int'>, {'a': 10, 'c': 13, 'b': 12})

或者

>>> kwargs = {'a':10,'b':12,'c':13}
>>> d_int = defaultdict(int, **kwargs)
>>> d_int
defaultdict(<type 'int'>, {'a': 10, 'c': 13, 'b': 12})

它是如何工作的

作为标准字典的子类,它可以执行所有相同的功能。

但是,在传递未知键时,它会返回默认值而不是错误。例如:

>>> d_int['a']
10
>>> d_int['d']
0
>>> d_int
defaultdict(<type 'int'>, {'a': 10, 'c': 13, 'b': 12, 'd': 0})

如果您想更改默认值,请覆盖default_factory:

>>> d_int.default_factory = lambda: 1
>>> d_int['e']
1
>>> d_int
defaultdict(<function <lambda> at 0x7f34a0a91578>, {'a': 10, 'c': 13, 'b': 12, 'e': 1, 'd': 0})

或者

>>> def foo():
...     return 2
>>> d_int.default_factory = foo
>>> d_int['f']
2
>>> d_int
defaultdict(<function foo at 0x7f34a0a0a140>, {'a': 10, 'c': 13, 'b': 12, 'e': 1, 'd': 0, 'f': 2})

问题中的例子

例子1

由于int被作为default_factory传递,任何未知的键默认返回0。

现在,在循环中传递字符串,它将增加d中这些字母的计数。

>>> s = 'mississippi'
>>> d = defaultdict(int)
>>> d.default_factory
<type 'int'>
>>> for k in s:
...     d[k] += 1
>>> d.items()
[('i', 4), ('p', 2), ('s', 4), ('m', 1)]
>>> d
defaultdict(<type 'int'>, {'i': 4, 'p': 2, 's': 4, 'm': 1})

作为默认工厂传递了一个列表,因此任何未知(不存在的)键都将默认返回 [ ](即列表)。
现在,由于元组列表在循环中传递,它将在 d[color] 中追加值。
>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> d.default_factory
<type 'list'>
>>> for k, v in s:
...     d[k].append(v)
>>> d.items()
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
>>> d
defaultdict(<type 'list'>, {'blue': [2, 4], 'red': [1], 'yellow': [1, 3]})

谢谢你的回答。你知道如何让常量始终不同吗?我解释一下:defaultdict(lambda: 'string', **kwargs) 不会按预期工作,因为所有新键都将共享“string”的相同实例。我该如何每次提供一个副本?请注意,defaultdict(lambda: copy.copy('string'), **kwargs) 不起作用,因为copy只被评估一次。 - Dr_Zaszuś

41

字典是一种方便的方式,可以通过名称(键)存储数据以供以后检索。键必须是唯一的、不可变的对象,通常是字符串。字典中的值可以是任何内容。对于许多应用程序而言,这些值都是简单类型,如整数和字符串。

当字典中的值是集合(列表、字典等)时,它变得更有趣。在这种情况下,值(空列表或字典)必须在第一次使用给定键时进行初始化。虽然手动执行此操作相对容易,但defaultdict类型自动化并简化了这些操作。
defaultdict与普通dict完全相同,但其使用一个函数(“默认工厂”)进行初始化,该函数不接受任何参数并为不存在的键提供默认值。

defaultdict永远不会引发KeyError。任何不存在的键都将获得由默认工厂返回的值。

from collections import defaultdict
ice_cream = defaultdict(lambda: 'Vanilla')

ice_cream['Sarah'] = 'Chunky Monkey'
ice_cream['Abdul'] = 'Butter Pecan'

print(ice_cream['Sarah'])
>>>Chunky Monkey

print(ice_cream['Joe'])
>>>Vanilla

这里是另一个关于如何使用defaultdict减少复杂度的示例

from collections import defaultdict
# Time complexity O(n^2)
def delete_nth_naive(array, n):
    ans = []
    for num in array:
        if ans.count(num) < n:
            ans.append(num)
    return ans

# Time Complexity O(n), using hash tables.
def delete_nth(array,n):
    result = []
    counts = defaultdict(int)

    for i in array:
        if counts[i] < n:
            result.append(i)
            counts[i] += 1
    return result


x = [1,2,3,1,2,1,2,3]
print(delete_nth(x, n=2))
print(delete_nth_naive(x, n=2))
在结论中,每当您需要使用字典且每个元素的值应该以默认值开始时,请使用defaultdict。

最后,一个清晰、简单且符合Python风格的例子。谢谢。 - Ed Randall

20

这里有一个关于defaultdict的很好的解释:http://ludovf.net/blog/python-collections-defaultdict/

基本上,参数intlist是你传递的函数。记住Python可以接受函数名作为参数。int默认返回0,而list在调用时带括号会返回一个空列表。

在普通字典中,如果在你的例子中我试图调用d[a],我会得到一个错误(KeyError),因为只有键m、s、i和p存在,而键a尚未初始化。但在defaultdict中,它将函数名作为参数,当你尝试使用未初始化的键时,它简单地调用你传递的函数并将其返回值分配给新键的值。


18

可以通过在每次调用d[key]的地方改为使用dict.setdefault来轻松模拟defaultdict的行为。

换句话说,代码如下:

from collections import defaultdict

d = defaultdict(list)

print(d['key'])                        # empty list []
d['key'].append(1)                     # adding constant 1 to the list
print(d['key'])                        # list containing the constant [1]

等价于:

d = dict()

print(d.setdefault('key', list()))     # empty list []
d.setdefault('key', list()).append(1)  # adding constant 1 to the list
print(d.setdefault('key', list()))     # list containing the constant [1]

唯一的区别在于,使用defaultdict时,列表构造函数只会被调用一次,而使用dict.setdefault时,列表构造函数会被调用多次(但如果真的需要的话,可以重写代码以避免这种情况)。

有些人可能会争论存在性能上的考虑,但是这个问题是一个雷区。例如,这篇文章显示使用defaultdict并没有太大的性能提升。

我认为,defaultdict是一种在代码中增加混乱而不是好处的集合。对我来说是无用的,但其他人可能会有不同的看法。


12

由于这个问题涉及“如何工作”,一些读者可能希望看到更多的细节。具体而言,所讨论的方法是__missing__(key)方法。请参见:https://docs.python.org/2/library/collections.html#defaultdict-objects

更具体地说,此答案展示了如何实际运用__missing__(key)

https://dev59.com/3WMl5IYBdhLWcg3w_rKD#17956989

为了澄清“callable”的含义,这里有一个交互式会话(来自2.7.6,但在v3中应该也可以使用):

>>> x = int
>>> x
<type 'int'>
>>> y = int(5)
>>> y
5
>>> z = x(5)
>>> z
5

>>> from collections import defaultdict
>>> dd = defaultdict(int)
>>> dd
defaultdict(<type 'int'>, {})
>>> dd = defaultdict(x)
>>> dd
defaultdict(<type 'int'>, {})
>>> dd['a']
0
>>> dd
defaultdict(<type 'int'>, {'a': 0})

那是defaultdict最典型的用法(除了毫无意义地使用x变量)。您可以使用0作为显式默认值完成相同的操作,但不能使用简单值:

>>> dd2 = defaultdict(0)

Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    dd2 = defaultdict(0)
TypeError: first argument must be callable

相反,以下代码有效,因为它传递了一个简单的函数(它会即时创建一个无名称函数,该函数不带参数并始终返回 0):

相反,以下工作是因为它传入了一个简单的函数(它在创建一个无名称函数,并接受没有参数且总是返回0的值):

>>> dd2 = defaultdict(lambda: 0)
>>> dd2
defaultdict(<function <lambda> at 0x02C4C130>, {})
>>> dd2['a']
0
>>> dd2
defaultdict(<function <lambda> at 0x02C4C130>, {'a': 0})
>>> 

并且使用不同的默认值:

>>> dd3 = defaultdict(lambda: 1)
>>> dd3
defaultdict(<function <lambda> at 0x02C4C170>, {})
>>> dd3['a']
1
>>> dd3
defaultdict(<function <lambda> at 0x02C4C170>, {'a': 1})
>>> 

11

我的个人建议:你也可以对defaultdict进行子类化:

class MyDict(defaultdict):
    def __missing__(self, key):
        value = [None, None]
        self[key] = value
        return value

这对于非常复杂的情况可能会很有用。


10

好的,defaultdict在以下情况下也可能引发键错误:

from collections import defaultdict
d = defaultdict()
print(d[3]) #raises keyerror

永远记得给defaultdict传递参数,例如:

d = defaultdict(int)

9

defaultdict工具是Python中collections类中的容器。它类似于通常的字典(dict)容器,但有一个区别:在初始化时指定了值字段的数据类型。

例如:

from collections import defaultdict

d = defaultdict(list)

d['python'].append("awesome")

d['something-else'].append("not relevant")

d['python'].append("language")

for i in d.items():

    print i

这将打印:

('python', ['awesome', 'language'])
('something-else', ['not relevant'])

“值字段的数据类型在初始化时指定”这是不正确的。提供了一个元素工厂函数。在这里,list是要调用以填充缺失值而不是创建对象类型。例如,要将默认值设置为1,您将使用显然不是类型的lambda:1 - asachet

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