想要认识一门语言,不单只是就语法部份,认识的设计与风格,也是极为重要的一部份。
在标准程序库为数众多的API中,从群集API的资料结构分类与阶层作为开始,是个不错的方式,对于Python这类将常用资料结构作为内建型态的语言,更是如此。
从Python内建型态谈起
就语言来说,常用的资料结构,都会有一套标准实作。
有些语言将常用资料结构内建为语法的一部份,象是Python的list、set、tuple、dict等,都可使用实字方式撰写,这让程序变得简洁,也能应付大多数处理资料的需求;有些语言将这些实作以独立的API实作,象是Java的Collection、Map API等,让这类资料结构用来与程序语法泾渭分明,虽然语法囉嗦,然而,开发者较易(或说是强制要)掌握型态与实作品的特性。
对于熟悉Java的我而言,在使用Python语言,享用过这类资料结构作为直译器内建型态的好处之后,不免开始有了一些蒙胧的疑问,这些内建型态的实作特性呢?我很熟悉Java中Collection、Map API的设计架构,那么Python中list、set、tuple、dict的分类与阶层呢?
若不能掌握API设计架构,不但难以掌握各资料结构实作品的特性,也容易沦为死记API的窘境,更容易撰写出没有弹性的程序。
由于熟悉Java,一开始不免想要将Java中的界面与实作品,逐一在Python中找出对照物,不过,不久就发现方向似乎有点错误,毕竟两门语言的设计哲学不同,更何况是API的设计了,最大的差别之一是,Python是动态定型,而Java是静态定型语言,因此Python没有Java中Collection、Map那样的界面阶层,而是直接思考行为,将相关资料结构分为了Sequences、Set与Mapping三种型态。
对于Sequence,它们都具有索引取值、切片等一些共同的行为,Python接着将Sequence进一步地区分了Immutable与Mutable两种,str、tuple、bytes属于Immutable,而list、bytearray属于Mutable,并具有Immutable几乎全部的行为,以及切片操作指定、append、remove等操作,然而,Immutable具备、但Mutable不一定会实作的行为是hash操作,因此Immutable的型态就可以作为dict的键,或者是set、frozenset的元素。
collections模块
所谓Sequence型态,并不是指它们真的有继承一个Sequence类别,而是指具备Sequence的行为,具体而言,就是像实作了len、index这类的操作罢了,这让设计函式时更有弹性;进一步地,我开始想到‥set、dict的行为,分别看来就象是Java中的HashSet、HashMap,都是无序的,那么有没有TreeSet、TreeMap呢?对于这类需求,Python提供了collections模块来满足。
在collections模块中,可基于dict建立一个OrderedDict,它看来象是Java中的TreeMap,不过是以元素插入顺序来定序,这表示,若想在取得键值对时,能依自订顺序来取出,就必须先在一个Sequence中自行对键值排序,再用排序后的结果建立OrderedDict。
在Java的Collection架构中,有个Queue界面与子界面Deque,而在Python中,list本身可实现Queue甚至是Deque,不过要注意到效能问题——当从list前端取出或新增元素时,时间复杂度会是O(n),如果元素数量多的话,最好是改用collections模块中的deque,它的popleft、appendleft时间复杂度会是O(1),当然,作为双向队列的实现,deque也提供了rotate之类的方法。
在Python中,tuple的地位总是比较尴尬,很多层面它跟list类似,只不过它是Immutable,占用比较少的资源;然而,如果看过Haskell的Tuple,就会知道Tuple的元素型态,相当于临时组成了未命名的新型态,其实在Python的collections模块中,也有个类似的namedtuple函式,可让既有的tuple跟Haskell中的Tuple类似,充当临时的资料结构。例如:
Point = namedtuple('Point', ['x', 'y'])
p = Point(11, 22)
print(p.x + p.y) #
显示
33
namedtuple是个工厂函式,传回的实际上是动态建立的tuple子类别,因此具有tuple的行为,然而具有指定的型态名称与栏位名称,也可以使用dot来存取栏位,这让它的实例就象是个Immutable物件,比一般物件节省资源,也可以充当dict来使用。
collections.abc模块
当然,collections中,还有象是Counter类别、defaultdict函式等可以使用,不过,终究还是没有发现象是Java中LinkedList、TreeSet这类的东西。如果需要这类东西,必须要自行实作。例如,若需要list的全部功能,而且,在某些场合必须被判断为list实例(象是使用isinstance()判断,例如OrderedDict就是dict的子类别),那么可以继承list;如果只是要包裹一个list,并让包裹器行为上像个list,可以继承UserList。
然而,如果只是要实作群集的行为,但不需要是群集中某个类别的子类别时,可以继承collections.abc模块中的相关类别——abc代表着abstract base classes,也就是这些类别是作为抽象基础类别,它们本身已经实作了一些行为。不过,还有些必要的抽象方法,必须开发者在继承之后自行定义,如果开发者忘了定义,建立实例时会引发TypeError,并告知忘了实作哪些抽象方法,这开发者免于记忆或查询有哪些方法必须实作。
而且,一旦开发者定义了必要的抽象方法,基于这些抽象方法的其他方法实作(Mixin Methods),就能够运作了,这会比完全自定义一个群集类别来得方便许多且不容易出错,比方说,在Python的collections.abc官方说明文件中,就有个〈OrderedSet recipe〉的文件链结,其中的OrderedSet,就是基于collections.MutableSet而实作。
collections.abc模块中,还有Iterable、Iterator、Hashable等抽象基础类别,想要清楚地认识Python中的群集特性与行为,认识collections.abc模块是个相当好的管道,规范抽象基础类别的PEP 3119规格书,也非常值得参考。
若想再更进一步,我们可以针对collections、collections.abc中,研究相关函式或类别的程序码实现,这样将更能够掌握相关特性的实现细节。
清楚而自信地使用资料结构
Pascal之父Niklaus E. Writh曾经说过:「算法加资料结构就等于程序」,因此是否了解常用的、特性与相关的效用函式,对程序的设计影响巨大。这在我先前专栏〈开发者应认识的资料型态及效用函式〉、以及〈善用程序库中的资料结构特性〉,也曾经探讨过。
因此,面对像Python这类将资料结构内建为语法一部份的语言,虽然一开始用起来顺手且具有高产能,然而,最好还是循着对资料结构特性的基本认识,逐一探讨这些便捷语法背后的设计与实现,甚至必要时,可基于标准程序库提供的基础,试着自行实现相关资料结构。
这么一来,不但能对相关语法或程序库能有更深入的了解,且进一步地认识语言及API的相关设计哲学,在需要这些资料结构时,也才能清楚而有自信地加以运用。