最近折腾索引引擎以及数据统计方面的工作比较多, 与 Python 字典频繁打交道, 至此整理一份此方面 API 的用法与坑法备案.
索引引擎的基本工作原理便是倒排索引, 即将一个文档所包含的文字反过来映射至文档; 这方面算法并没有太多花样可言, 为了增加效率, 索引数据尽可往内存里面搬, 此法可效王献之习书法之势, 只要把十八台机器内存全部塞满, 那么基本也就功成名就了. 而基本思路举个简单例子, 现在有以下文档 (分词已经完成) 以及其包含的关键词
1
2
3
|
doc_a: [word_w, word_x, word_y]
doc_b: [word_x, word_z]
doc_c: [word_y]
|
将其变换为
1
2
3
4
|
word_w - > [doc_a]
word_x - > [doc_a, doc_b]
word_y - > [doc_a, doc_c]
word_z - > [doc_b]
|
写成 Python 代码, 便是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
doc_a = { 'id' : 'a' , 'words' : [ 'word_w' , 'word_x' , 'word_y' ]}
doc_b = { 'id' : 'b' , 'words' : [ 'word_x' , 'word_z' ]}
doc_c = { 'id' : 'c' , 'words' : [ 'word_y' ]}
docs = [doc_a, doc_b, doc_c]
indices = dict ()
for doc in docs:
for word in doc[ 'words' ]:
if word not in indices:
indices[word] = []
indices[word].append(doc[ 'id' ])
print indices
|
不过这里有个小技巧, 就是对于判断当前词是否已经在索引字典里的分支
1
2
|
if word not in indices:
indices[word] = []
|
可以被 dict 的 setdefault(key, default=None) 接口替换. 此接口的作用是, 如果 key 在字典里, 那么好说, 拿出对应的值来; 否则, 新建此 key , 且设置默认对应值为 default . 但从设计上来说, 我不明白为何 default 有个默认值 None , 看起来并无多大意义, 如果确要使用此接口, 大体都会自带默认值吧, 如下
1
2
3
|
for doc in docs:
for word in doc[ 'words' ]:
indices. setdefault(word, []) .append(doc[ 'id' ])
|
这样就省掉分支了, 代码看起来少很多.
不过在某些情况下, setdefault 用起来并不顺手: 当 default 值构造很复杂时, 或产生 default 值有副作用时, 以及一个之后会说到的情况; 前两种情况一言以蔽之, 就是 setdefault 不适用于 default 需要惰性求值的场景. 换言之, 为了兼顾这种需求, setdefault 可能会设计成
1
2
3
4
|
def setdefault( self , key, default_factory):
if key not in self :
self [key] = default_factory()
return self [key]
|
倘若真如此, 那么上面的代码应改成
1
2
3
|
for doc in docs:
for word in doc[ 'words' ]:
indices.setdefault(word, list ).append(doc[ 'id' ])
|
不过实际上有其它替代方案, 这个最后会提到.
如果说上面只是一个能预见但实际上可能根本不会遇到的 API 缺陷, 那么下面这个就略打脸了.
考虑现在要进行词频统计, 即一个词在文章中出现了多少次, 如果直接拿 dict 来写, 大致是
1
2
3
4
5
6
7
|
def word_count(words):
count = dict ()
for word in words:
count.setdefault(word, 0 ) + = 1
return count
print word_count([ 'hiiragi' , 'kagami' , 'hiiragi' , 'tukasa' , 'yosimizu' , 'kagami' ])
|
当你兴致勃勃地跑起上面代码时, 代码会以迅雷不及掩脸之势把异常甩到你鼻尖上 --- 因为出现在 += 操作符左边的 count.setdefault(word, 0) 在 Python 中不是一个左值. 怎样, 现在开始念叨 C艹 类型体系的好了吧.
因为 Python 把默认的字面常量 {} 等价于 dict() 就认为 dict 是银弹的思想是要不得的; Python 里面各种数据结构不少, 解决统计问题, 理想的方案是 collections.defaultdict 这个类. 下面的代码想必看一眼就明白
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from collections import defaultdict
doc_a = { 'id' : 'a' , 'words' : [ 'word_w' , 'word_x' , 'word_y' ]}
doc_b = { 'id' : 'b' , 'words' : [ 'word_x' , 'word_z' ]}
doc_c = { 'id' : 'c' , 'words' : [ 'word_y' ]}
docs = [doc_a, doc_b, doc_c]
indices = defaultdict( list )
for doc in docs:
for word in doc[ 'words' ]:
indices[word].append(doc[ 'id' ])
print indices
def word_count(words):
count = defaultdict( int )
for word in words:
count[word] + = 1
return count
print word_count([ 'hiiragi' , 'kagami' , 'hiiragi' , 'tukasa' , 'yosimizu' , 'kagami' ])
|
完满解决了之前遇到的那些破事.
此外 collections 里还有个 Counter , 可以粗略认为它是 defaultdict(int) 的扩展.