6.6 创建一种新的映射
Python中内置了dict映射,在库中也有许多映射类型。除了collections模块对dict的扩展(defaultdict、Counter和ChainMap)之外,库中还有一些模块包含了类似于映射的结构。
shelve模块是其他映射的一个重要示例。我们会在第10章“用Shelve保存和获取对象”中介绍它。dbm模块与shelve类似,也是将一个键映射到一个值上。
mailbox和email.message模块中的类为邮箱提供了一个类似于dict的接口,这个接口被用于管理本地邮件。
随着我们介绍越来越多的设计原则,可以用扩展或者封装的方式向映射中添加更多功能。
可以升级Counter类,将平均数和标准差用作频率分布数据存储。实际上,也能从这个类中很容易地算出中位数和众数。
下面的StatsCounter是对Counter的一个扩展,它加入了一些用于统计的函数。
from collections import Counter
class StatsCounter(Counter):
@property
def mean( self ):
sum0= sum( v for k,v in self.items() )
sum1= sum( kv for k,v in self.items() )
return sum1/sum0
@property
def stdev( self ):
sum0= sum( v for k,v in self.items() )
sum1= sum( kv for k,v in self.items() )
sum2= sum( kkv for k,v in self.items() )
return math.sqrt( sum0sum2-sum1sum1 )/sum0
我们向Counter类中添加了两个根据频率分布计算平均数和标准差的方法。这里的公式和前面基于list对象的主动计算中用的类似,尽管这里是基于Counter对象的延迟计算。
我们用sum0= sum( v for k,v in self.items() )来计算值v的和,并忽略了键k。我们可以用一个下划线(_)代替k来强调我们要忽略键。可以用sum( v for v in self.values() )来强调我们不准备使用键。但是我们更倾向于对sum0和sum1使用平行的结构。
我们可以用这个类高效地对原始数据进行统计和定量分析,运行大量的模拟,然后用Counter对象收集结果。
这里是与列表中代表真实结果的样本数据的交互。
>>> sc = StatsCounter( [2, 4, 4, 4, 5, 5, 7, 9] )
>>> sc.mean
5.0
>>> sc.stdev
2.0
>>> sc.most_common(1)
[(4, 3)]
>>> list(sorted(sc.elements()))
[2, 4, 4, 4, 5, 5, 7, 9]
most_common()的结果是一个包含两个元素的元组,其中一个是众数(4),另一个是这个值出现的次数(3)。我们可能想获取前3个众数,这样结果中就会包括另外两个出现频率没有这么高的元素。通过执行类似sc.most_common(3),就能获得出现频率最高的几个值。
elements()方法按原始数据中所有元素的顺序重建列表。
从排好序的元素中,我们可以获得中位数,就是位于最中间的元素。
@property
def median( self ):
all= list(sorted(sc.elements()))
return all[len(all)//2]
这个方法不仅会是延迟执行,而且它会消耗很多内存;它仅仅为了找到中位数就用所有的值创建了一个完整的序列。
尽管它很简单,但是这是一种昂贵的使用Python的方式。
一种更明智的做法是用sum(self.values())//2计算有效长度和中点。一旦我们知道了这两个信息,就可以按顺序访问键,并计算出一个给定键位于哪个区域。最后,会在包括了中间点的区域中找到这个键。
代码类似于下面这样。
@property
def median( self ):
mid = sum(self.values())//2
low= 0
for k,v in sorted(self.items()):
if low <= mid < low+v: return k
low += v
我们通过键和它们出现的次数定位最中间的键。注意,这里使用了内置的sorted函数,所以还需要加上它带来的开销。
通过timeit,我们可以知道前面那个挥霍内存的版本需要9.5秒,这个更明智的版本只需要5.2秒。
