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