Python defaultdict与Counter介绍

原生字典的特性

作为一种使用键值对形式存储的容器,Python原生的字典存在一个特性:当访问字典中不存在的键的时候,会抛出一个KeyError错误,如下。

1
2
3
4
5
6
7
>>> my_dict = {'key1': 'value1', 'key2': 'value2'}
>>> my_dict
{'key1': 'value1', 'key2': 'value2'}
>>> my_dict['key3']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'key3'

这样的特性确实挺合理,但在实际使用中的某些场景下存在不便。某些情况下,我希望我访问字典中不存在的键的时候不抛出异常,而是返回一个默认值。例如,下面的例子中,使用字典char_count统计字符串Hello world!中字符出现的次数。

1
2
3
4
5
6
7
8
9
10
>>> char_count = {}
>>> my_str = 'Hello world!'
>>> for c in my_str:
... if c in char_count:
... char_count[c] += 1
... else:
... char_count[c] = 1
...
>>> char_count
{'H': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1}

可以看到,在计数时,需要判断字符c是否在字典中有对应的记录,如果没有的话得先创建这个键,代码较为繁琐。另外,当我想查询一个没有在my_str字符串中出现的字符(例如a)出现的次数时,就不能直接访问char_count['a'],需要使用char_count.get('a', 0)的方式获取。字典对象的get方法接受两个参数,第一个参数表示想要访问的键,第二个值表示当这个键不存在时的返回值。

1
2
3
4
5
6
>>> char_count['a']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'a'
>>> char_count.get('a', 0)
0

但显然,这样的代码非常繁琐,每次都要指定一个默认值,而且get出来的值也无法放在等号左边,作为左值,以修改字典中该键对应的内容。为解决此问题,需要一种自带默认值的字典,即下面要介绍的defaultdict

defaultdict

defaultdict正是上面介绍的自带默认值的字典。与普通字典最大的不同在于,他在创建的时候接受一个函数作为参数,该函数用于生成,当访问的键不存在时该键的默认值。

defaultdict在自带的collections模块中,使用from collections import defaultdict方式引入。基本使用方法如下。

1
2
3
4
5
6
7
>>> from collections import defaultdict
>>> my_dd = defaultdict(lambda: 'default') # return 'default' by default
>>> my_dd['key1'] = 'value1'
>>> my_dd['key1'] # 'key1' exists, return 'value1'
'value1'
>>> my_dd['key2'] # 'key2' not exists, return 'default' by default
'default

注意,创建defaultdict时传入的应当是一个函数,用于生成默认值(作为返回值返回),而不是默认值本身。除了键不存在时返回默认值外,其他行为与原生字典没有区别。

注意,上述描述中,“键不存在时返回默认值”的说法不够严谨。更准确的行为描述应当是,对于dd = defaultdict(func),当第一次访问一个不存在的键key时,会首先立刻执行dd[key] = func(),即给此key创建一个键值对,值通过调用func获得,然后再执行其他的操作。接着上面的例子,我们通过my_dd['key2']访问不存在的键'key2'之后,再去看my_dd,发现其已经包含'key2': 'default'这一对了(但其实我们并没有手动给key2赋值)。

1
2
>>> my_dd
defaultdict(<function <lambda> at 0x7fb8ea3b4b70>, {'key1': 'value1', 'key2': 'default'})

defaultdict创建时传入的函数可以是一个标准的使用def xxx():定义的函数,也可以是lambda表达式,也可以是诸如intstrlistsetdict此类的工厂函数,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> dd_int = defaultdict(int)
>>> dd_int['key']
0
>>> dd_str = defaultdict(str)
>>> dd_str['key']
''
>>> dd_list = defaultdict(list)
>>> dd_list['key']
[]
>>> dd_set = defaultdict(set)
>>> dd_set['key']
set()
>>> dd_dict = defaultdict(dict)
>>> dd_dict['key']
{}

基于以上特性,我们可以使用更简洁的代码实现统计字符串Hello world!中字符出现的次数,如下所示。

1
2
3
4
5
6
7
>>> char_count_dd = defaultdict(int)
>>> my_str = 'Hello world!'
>>> for c in my_str:
... char_count_dd[c] += 1
...
>>> char_count_dd
defaultdict(<class 'int'>, {'H': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1})

使用dict(dd)方式可以讲defaultdict类型的dd转换为普通的dict。可以看到,两种方法最终生成的dict是一致的。

1
2
3
4
>>> dict(char_count_dd)
{'H': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1}
>>> char_count
{'H': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1}

Counter

事实上,Python自带一个简单的计数器Counter,使用from collections import Counter方式引入,可以直接实现简单的计数任务,直接将需要计数的对象传入Counter即可。使用方法如下:

1
2
3
4
5
>>> from collections import Counter
>>> my_str = 'Hello world!'
>>> char_count_counter = Counter(my_str)
>>> char_count_counter
Counter({'l': 3, 'o': 2, 'H': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1})

Counter实际上也是原生字典的一个子类,因此也可以像defaultdict(int)一样使用。

1
2
3
4
5
6
7
>>> char_count_counter_dd = Counter()
>>> my_str = 'Hello world!'
>>> for c in my_str:
... char_count_counter_dd[c] += 1
...
>>> char_count_counter_dd
Counter({'l': 3, 'o': 2, 'H': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1, '!': 1})

除此之外,Counter还实现了很多别的方法,有一些别的特性。例如,可以在Counter对象上进行+-&|操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> c = Counter(a=3, b=1)
>>> c
Counter({'a': 3, 'b': 1})
>>> d = Counter(a=1, b=2)
>>> d
Counter({'b': 2, 'a': 1})
>>> c + d # add two counters together: c[x] + d[x]
Counter({'a': 4, 'b': 3})
>>> c - d # subtract (keeping only positive counts)
Counter({'a': 2})
>>> c & d # intersection: min(c[x], d[x])
Counter({'a': 1, 'b': 1})
>>> c | d # union: max(c[x], d[x])
Counter({'a': 3, 'b': 2})

更多内容查阅官方文档。

参考链接