第2章:抽象数据类型和Python类
2.1 抽象数据类型
抽象数据类型(Abstract Data Type,ADT)是计算机领域中被广泛接受的一种思想和方法,也是一种用于设计和实现程序模块的有效技术。ADT的基本思想是抽象,或者说是数据抽象。(数据抽象,与函数定义实现的计算抽象或称计算抽象,所相对应。)
2.1.1 数据类型与数据构造。
类型(数据类型),是程序设计领域最重要的基本概念之一,在程序里描述的、通过计算机去处理的数据,通常都分属于不同的类型,如整形或浮点型等等。每个类型包含一集的合法数据对象,并规定了对这些对象的合法操作。各种编程语言都有类型的概念,每种语言都提供了一组内置数据类型,为每个内置类型提供了一批操作。
以Python为例,它提供的基本类型包括逻辑类型bool,数值类型int和float等、字符串类型str,还有一些组合数据类型。但是,无论编程语言提供了多少内置类型,在处理较为复杂的问题时,程序员或早或晚都会遇到一些情况,此时各种内置类型都不能满足或者不能适合于自己的需要。
在这种情况下,编程语言提供的组合类型是可以解决一些问题的,如Python所提供的list,tuple,set,dict等结构。编程时可以利用它们把一组相关的数据组织在一起,构成一个数据对象,作为一个整体存储、传递和处理。
举个例子,假设程序需要处理有理数,最简单朴素的想法就是用两个整数分别表示一个有理数的分子和分母。在此基础上实现所需要的运算和操作,在这种安排下,把有理数3/5存入变量可能写成:
a1 = 3
b1 = 5
而利用Python函数可返回多对象元组和多项赋值的机制,加法函数可以如下定义:
def rational_plus(a1,b1,a2,b2):
num = a1*b2 + b1*a2
den = b1*b2
return num,den
下面是一个简单的函数使用实例:
In [2]: a2,b2 = rational_plus(a1,b1,7,10)
In [3]: a2,b2
Out[3]: (65, 50)
不难想到的是,如果真的这样去写程序,很快就会遇到非常麻烦的管理问题:这里用每两个单独的变量来为一个有理数赋值,那么,编程者需要时时刻刻记住哪两个变量记录的是一个有理数的分子和分母,操作时不能混淆不同的有理数;如果需要换一个有理数参与变量,也会遇到成对变量名的替换问题。而程序比较复杂时,做这类问题就很容易出错,一旦真的出错,确定错在哪里并改正也极其费时费力。
一种简单改进是利用编程语言的数据组合机制,把相关的多项简单数据组合在一起,还是看有理数的例子,可以考虑用一个Python元组(tuple)而非两个单独的变量来表示一个有理数,约定其中第0项表示分子,第1项表示分母,这样就可以写:
##用元组而非两个独立变量来表示一个有理数:
r1 = (3,5)
r2 = (7,10) #r1和r2分别表示了两个不同的有理数
def rational_plus(r1,r2):
num = r1[0]*r2[1] + r2[0]*r1[1]
den = r1[1]*r2[1]
return num,den
尝试使用该函数:
In [6]: r3 = rational_plus(r1,r2)
In [7]: r3
Out[7]: (65, 50)
现在的情况显然好了很多,许多管理问题得到了缓解,这就是数据构造和组织的作用。
但是,如果进一步考虑,就会发现这样做仍然有多方面的缺陷,例如:
- 这里使用的不是特殊的“有理数”而是普通的元组,因此不能将其与其他元组区别表示。例如,(3,5)表示平面上X坐标为3,Y坐标为5的点。从概念上说,把一个有理数和一个格点相加是非常荒谬的,但是Python编程语言,包括上面定义的函数rational_plus都不会认为这样做是错误的。
- 与有理数相关的操作并没有绑定于有理数的二元组,由于Python不需要说明函数参数的类型,这个问题更加严重。
- 在为有理数定义运算(函数)时,需要直接按位置去的元素。对有理数这样结构简单,在操作中只需要区分位置0,1的对象还算比较容易处理,给思维带来的负担也可以忍受。但是如果需要处理的数据对象更复杂,比如包含了十几个甚至几十个不同成员,在为这种组合数据对象定义操作的时候,记住每个成员在对象里的位置并正确适用,就变成一件非常麻烦的事情了。更不要提修改数据的时候。
以上是原书中的说法,概括一下就是两个问题:第一,没有把“有理数”这一特殊的类型,和其他的意义完全不同的元组类型区分开来;第二,由于前一项的缘故,所以关于有理数的操作也没有根据数据类型进行特化,而完全是从元组中读取下标的操作,并不方便和直观。
2.1.2 抽象数据类型的概念
造成前一节中揭示出的变成缺陷的最重要问题之一,就是数据的表示完全暴露,以及对象使用和操作实现对具体表示的依赖性。要克服这些缺点,就要把对象的使用与其具体实现隔离开来。
理想的情况是:在编程中使用一种对象时,只需要考虑“如何使用”,而不需要(最好是根本不能)去关注和触及对象的内部表示,这样的数据对象就是一种抽象单元。一组这样的对象构成一个抽象的数据类型,为程序里的使用提供了一整套功能。
抽象数据类型的基本想法,是把数据定义为抽象的对象集合,只为它们定义可用的合法操作,并不暴露其内部实现的具体细节,不论是其数据的表示细节还是操作的实现细节。当然,要使用一种对象,首先需要能够早这种对象,然后能操作它们。抽象数据类型的操作应该满足这些要求,一个数据类型的操作通常可以分为以下三类:
- 构造操作:这类操作基于一些已知信息,产生出这种类型的一个新对象。
- 解析操作:这种操作从一个对象取得有用的信息,其结果反映了被操作对象的某方面特性,但结果并不是本类型的对象。
- 变动操作:这类操作修改被操作对象的内部状态。
当然,一个抽象数据类型还应该有一个名字,用于代表这个类型。
其实,编程语言的一个内置类型,就可以看做是一个抽象数据类型。Python的字符串类型是一个典型实例:字符串对象有一种内部表示形式(无需对外公布),人们用Python编程序时并不依赖于实际表示(甚至不知道其具体表示方式);str提供了一组操作供编程使用,每个操作都有明确的抽象意义,不依赖于内部的具体实现技术。
作为数据类型,特别是很复杂的数据类型,有一个很重要的性质被称为变动性,表示该类型的对象,在创建之后是否允许变化。如果某个类型只提供上面的第1和第2类操作,那么该类型的对象在创建之后就不会变化,永远处于一个固定的状态。这样的类型被称为不变数据类型,这种类型的对象则被称为不变对象。对于这种类型,在程序里只能(基于其他信息或已有对象)构造新对象或者取得已有对象的特性,不能修改已经建立的对象。如果一个类型提供了第3类操作,对该类型的对象执行这种操作后,虽然对象依旧,但是它的内部状态已经改变,这样的类型就称为可变操作类型,其对象称为可变对象。下面经常把不变数据类型和可变数据类型分别简称为不变类型和可变类型。
2.1.3 抽象数据类型的描述
定义一个抽象数据类型,目的是要定义一类计算对象,它们具有某些特定的功能,可以在计算中使用,这类对象的功能体现为一组可以对它们使用的操作,当然,还需要为这一抽象数据类型确定一个类型名。
下面为抽象数据类型引进一种描述方式,其形式体现了抽象数据类型的主要特点。
在后面介绍各种数据结构的时候,有关章节也经常是先给出一个抽象数据类型的描述,写出这种描述的过程本身也很有意义,因为它能够帮助开发者理清对希望定义的数据类型的想法,清晰第表达出各方面的形势要求(如操作的名字,参数的个数和类型等等)和功能要求(希望这个操作完成什么样的计算,产生什么样的效果)。
现在考虑一个简单地有理数抽象数据类型,有下面描述:
ADT Rational: #定义有理数的抽象数据类型
Rational(int num,int num) #构造有理数num/den
+(Rational r1, Rational r2) #求出表示r1+r2的有理数
-(Rational r1, Rational r2) #求出表示r1-r2的有理数
*(Rational r1, Rational r2) #求出表示r1*r2的有理数
/(Rational r1, Rational r2) #求出表示r1/r2的有理数
num(Rational r1) #取得有理数r1的分子
den(Rational r1) #取得有理数r1的分母
这里用特殊名字ADT表示这是一个抽象数据类型的描述,随它之后给出被定义类型的名字。ADT定义的主要部分描述了一组操作,每个操作的描述由两个部分组成:首先是用标识符或者特殊符号的形式给出的操作名和操作的参数表,随后用类似Python注释的形式给出操作的功能描述。另请注意,在描述操作的参数时,可以考虑在参数名前写一个类型名,表示这个参数应该具有的类型,也可以省略,通过文字叙述说明。
具体到上面的抽象数据类型,其名字是Rational,其中共提供了7个操作。第一个操作以Rational作为名字,这种形式表示它是一个最基本的构造操作,从其他类型的参数出发构造本类型的对象。随后的几个算术运算也是构造操作,它们基于Ra