6.8 总结
在本章中,我们介绍了很多内置类。对于大多数设计来说,内置的集合类型是一个很好的开始。通常我们会以tuple、list、dict或者set开始。对于应用程序中的不可变对象,可以利用namedtuple()创建的对于tuple的扩展。
除了这些类之外,collections模块中还有其他可供我们使用的标准库类型。
- deque。
- ChainMap。
- OrderedDict。
- Defaultdict。
- Counter。
同时,我们有3种标准的设计原则。可以封装任何现存的类型,或者可以选择扩展一个类。
最后,我们也可以创造一种全新的集合。这需要定义许多方法名和特殊方法。
6.8.1 设计要素和折中方案
当使用容器和集合时,我们将以下步骤作为设计原则。
1.考虑序列、映射和集合的内置版本。
2.考虑collections模块中的库扩展和一些其他的集合类型,例如heapq、bisect和array。
3.考虑组合使用现有的类定义。在许多情况下,一个tuple对象的list或者是包含list的dict就已经提供了必需的功能。
4.考虑扩展前面提到的某个类来提供额外的方法或者属性。
5.考虑用封装一个现有结构的方式作为提供额外方法或者属性的另一个途径。
6.最后,考虑实现一个新的数据结构。通常,有很多现成的资料可供我们参考。可以从维基百科的文章开始阅读,例如:http://en.wikipedia.org/wiki/List_
一旦确定了设计方案,还剩两个部分需要评估。
- 接口要如何兼容我们的问题域,这相对来说是一个比较主观的决定。
- 用timeit评估数据结构是否运作良好,这是一个完全客观的结果。
避免优柔寡断是非常重要的。我们需要高效地找到合适的集合。
在大多数情况下,最好能分析一个现有的应用程序以发现哪些数据结构带来了性能瓶颈。在一些情况下,在开始实现之前,考虑一个数据结构的复杂性就能知道它是否适合某个特定问题。
或许最重要的考虑是这个:“为了获得最佳性能,避免搜索”。
这是集合和映射需要可哈希对象的原因。定位集合或者映射中的可哈希对象几乎不用花任何时间。通过一个值(不是索引)在一个list查找一个元素会花费大量的时间。
下面比较用错误的类似于集合的方式使用list和用正确的方式使用set。
>>> import timeit
>>> timeit.timeit( 'l.remove(10); l.append(10)', 'l =
list(range(20))' )
0.8182099789992208
>>> timeit.timeit( 'l.remove(10); l.add(10)', 'l = set(range(20))' )
0.30278149300283985
我们从list和set中删除、添加一个元素。
很明显,滥用list,让它执行类似于set的操作导致运行时间增加了2.7倍。
在第2个例子中,演示了滥用list,让它执行类映射的操作。这是基于一个真实的例子,原本的代码使用了两个平行的集合模拟映射中的键和值。
接下来比较正确地使用映射和用两个平行的list模拟映射,如下所示。
>>> timeit.timeit( 'i= k.index(10); v[i]= 0', 'k=list(range(20));
v=list(range(20))' )
0.6549435159977293
>>> timeit.timeit( 'm[10]= 0', 'm=dict(zip(list(range(20)),list(ran
ge(20))))' )
0.0764331009995658
在平行list中,我们用一个list查找一个值,然后再将值存储在第2个list中。另一个例子中,只是简单地更新一个映射。
很明显,在两个平行的list中执行查找和更新是一个可怕的错误。它比用list.index()定位一个元素多花了8.6倍的时间,因为后者通过映射和哈希码来定位一个元素。
6.8.2 展望
在下一章中,我们将仔细探讨内置的数值类型和如何创建新的数值类型。和容器一样,Python提供了大量的内置数值类型。当创建一种新的数值类型时,我们必须定义大量的特殊方法。
在介绍完数值类型之后,我们会探讨一些更复杂的设计技巧。会介绍如何创建自定义的装饰器并用它们来简化类定义。我们也会介绍如何使用mixin类定义,这和抽象基类的定义类似。
