6.6 创建一种新的映射

    Python中内置了dict映射,在库中也有许多映射类型。除了collections模块对dict的扩展(defaultdictCounterChainMap)之外,库中还有一些模块包含了类似于映射的结构。

    shelve模块是其他映射的一个重要示例。我们会在第10章“用Shelve保存和获取对象”中介绍它。dbm模块与shelve类似,也是将一个键映射到一个值上。

    mailboxemail.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( k
    v 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() )来强调我们不准备使用键。但是我们更倾向于对sum0sum1使用平行的结构。

    我们可以用这个类高效地对原始数据进行统计和定量分析,运行大量的模拟,然后用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秒。