Chapter 2 Building Abstract ions with Data
Chapter 2 Building Abstract ions with Data
Native Data Types
到目前为止,我们使用的值都是 Python 语言中内置的少量的原始数据类型的实例。原始数据类型具有以下属性: ^13b410
- 有一些可以求解为原始数据类型的表达式,被称为字面量(literals)。
- 有用于操作原始类型值的内置函数和操作符。
python包含三种原始数据类型:整数(int)
,浮点数(float)
,复数(complex)
>>> type(1,5)
<class 'float'>
>>> type(1+1j)
<class 'complex'>
Data abstraction
当我们希望在程序中表示世界上广泛的事物时,会发现它们中的大多数都具有复合结构。比如地理位置具有经纬度坐标。为了表示位置,我们希望我们的编程语言能够经度和纬度耦合在一起形成一对复合数据,使它能够作为单个概念单元被程序操作,同时也能作为可以单独考虑的两个部分。
使用复合数据可以使程序更加模块化。如果我们能够将地理位置作为整体值进行操作,那么我们就可以将计算位置的程序部分与位置如何表示的细节隔离开来,这种将 “数据表示” 与“数据处理”的程序隔离的通用技术是一种强大的设计方法,称为数据抽象。数据抽象会使程序更易于设计、维护和修改。
数据抽象与函数抽象类似。当我们创建一个函数抽象时,函数实现的细节可以被隐藏,而特定的函数本身可以被替换为具有相同整体行为的任何其他函数。换句话说,我们可以创建一个抽象来将函数的使用方式与实现细节分离。类似地,数据抽象可以将复合数据值的使用方式与其构造细节隔离开来。
数据抽象的基本思想是构建程序,以便它们对抽象数据进行操作。也就是说,我们的程序应该以尽可能少的假设来使用数据,同时要将具体的数据表示定义为程序的独立部分。
程序的 “操作抽象数据” 和“定义具体表示”两个部分,会由一组根据具体表示来实现抽象数据的函数相连。为了说明该技术,我们将思考如何设计一组用于操作有理数的函数。
2.2.1 示例:有理数
有理数是整数的比值,并且有理数是实数的一个重要子类。 1/3
或 17/29
等有理数通常写为:
\(\frac{<numerator>}{<denmoinator>}\)
其中 <分子>
和 <分母>
都是整数值的占位符,这两个部分能够准确表示有理数的值。实际上的整数除以会产生 float
近似值,失去整数的精确精度
通过使用函数抽象,我们可以在实现程序的某些部分之前开始高效地编程。我们首先假设已经存在了一个从分子和分母构造有理数的方法,再假设有方法得到一个给定有理数的分子和分母。进一步假设得到以下三个函数:
rational(n, d)
返回分子为n
、分母为d
的有理数numer(x)
返回有理数x
的分子denom(x)
返回有理数x
的分母
我们在这里使用了一个强大的程序设计策略:一厢情愿(wishful thinking)。即使我们还没有想好有理数是如何表示的,或者函数 numer
、denom
和 rational
应该如何实现。但是如果我们确实定义了这三个函数,我们就可以进行加法、乘法、打印和测试有理数是否相等
现在我们有了选择器函数 numer
和 denom
以及构造函数 rational
定义的有理数运算,但还没有定义这些函数。我们需要某种方法将分子和分母粘合在一起形成一个复合值。
列表
2.2.2 对
为了使我们能够实现具体的数据抽象,Python 提供了一个名为 list
列表的复合结构,可以通过将表达式放在以逗号分隔的方括号[]
内来构造。这样的表达式称为列表字面量。
>>> [10,20]
[10,20]
元素选择运算符的等效函数称为 getitem
,它也使用 0 索引位置从列表中选择元素。
>>> from operator import getiem
>>> getiem(pair,0)
>>> getiem(pair,1)
序列
一个序列 (sequence) 是一组有序的值的集合。序列在计算机科学中是一个强大且基本的抽象概念。序列不是特定内置类型或抽象数据表示的实例,而是一个集合,集合中包含不同类型数据间的共享行为。也就是说,序列有很多种,但它们都有共同的行为。特别是:
长度:序列的长度是有限的。空序列的长度为 0。
元素选择:序列中的每个元素都对应一个非负整数索引。索引从 0 开始,从第一个元素依次对应。索引小于序列的长度。
Python 包括一些属于是序列的内置数据类型,其中最重要的是 list 列表。
2.3.1 列表
一个 list 值是一个可以有任意长度的序列。list 有大量的内置行为,以及表达这些行为的特定语法。我们已经见过文字列表,它的计算结果是一个 list 实例;以及一个元素选择表达式,该表达式的计算结果是列表中的一个值。内置的 len
函数返回序列的长度。如下,digits
是一个包含四个元素的列表。索引为 3 的元素是 8。
>>> digits = [1, 8, 2, 8]
>>> len(digits)
>>> digits[3]
此外,list 间可以相加,并且 list 可以乘以整数。对于序列,加法和乘法并不是对元素进行的,而是对序列自身进行组合和复制。也就是说,operator
模块中的 add
函数(和 +
运算符)会返回一个新列表,新列表中串联着添加的参数。operator
中的 mul
函数(和 *
运算符)会用原列表和整数 k 返回一个新列表,这个新列表中包含原列表的 k 次重复。
>>> [2, 7] + digits * 2
[2, 7, 1, 8, 2, 8, 1, 8, 2, 8]
任何值都可以包含在一个列表中,包括另一个列表。在包含列表的列表中,可以应用多次元素选择,以选择深度嵌套的元素。
>>> pairs = [[10, 20], [30, 40]]
>>> pairs[1]
[30, 40]
>>> pairs[1][0]
序列遍历
在许多情况下,我们希望遍历序列的元素并依次对每个元素执行一些计算。这种情况非常普遍,以至于 Python 有一个额外的控制语句来处理序列的数据:for 语句。
序列解包 (Sequence unpacking)。程序中的一个常见情况是序列中的元素也是序列,同时长度是固定一样的。for 语句可能在 header 中包含多个名称,来 “解包” 每个元素序列为各自的元素。例如,我们可能有一个包含两个元素列表的列表。
>>> pairs=[[1,2],[2,2],[2,3],[4,4]]
这种绑定多个名称到固定长度序列中的多个值的模式称为序列解包;这与赋值语句中将多个名称绑定到多个值的模式类似。
范围 (Ranges)。范围是 Python 中的另一种内置序列类型,它表示一个整数范围。范围是用 range
创建的,它有两个整数参数:范围的起始数字、范围的末尾数字再加一。
2.3.3 序列处理
序列是一种十分常见的复合数据形式,常见到甚至整个程序会都围绕着这个单一的抽象来组织。具有序列作为输入和输出的模块化组件可以混合和匹配以执行数据处理。将一系列序列处理操作链接在一起,就可以定义复杂的组件,而其中每个操作都是简单和有针对性的。
列表推导式 (List Comprehensions)。许多序列操作可以被理解为:为序列中的每个元素运算一个固定表达式,并将结果值收集在结果序列中。在 Python 中,列表推导式是执行此类计算的表达式。
>>> odds=[1,3,5,7,9]
>>> [x+1 for x in odds]
[2,4,6,8,10]
列表推导式的一般形式是:
[<map expression> for <name> in <sequence expression> if <filter expression>]
为了运算列表推导式,Python 首先评估 <sequence expression>
,它必须返回一个 iterable 值。然后,每个元素依次绑定到 <name>
,再运算 <filter expression>
;如果产生一个真值,运算 <map expression>
。最后 <map expression>
的值被收集到一个列表中。
聚合 (Aggregation)。序列处理中的第三种常见模式是将序列中的所有值聚合为一个值。内置函数 sum
、 min
和 max
都是聚合函数的示例。
结合上述三种模式:每个元素运算一个固定表达式、筛选元素、和聚合元素,我们就可以使用序列处理方法解决问题。
完美数是等于其约数之和的正整数。n
的除数是小于 n
的正整数,并可以整除 n
。列出 n
的除数可以用列表推导式来表示。
>>> def divisors(n):
>>> return [1]+[x for x in range(2,n) if n%x==0]
通过 divisors
,我们可以使用另一个列表推导式来计算从 1 到 1000 的所有完美数字。(1 通常也被认为是一个完美的数字,尽管它不符合我们对 divisors
的定义。)
高阶函数 (Higher-Order Functions)。序列处理中的常见模式可以使用高阶函数来表示。首先,可以通过将函数应用于每个元素来表示对序列中每个元素的表达式求值。
>>> def apply_to_all
>>> return [map_fn(x) for x in s]
如果想仅选择表达式返回真值的元素,可以通过对每个元素应用函数来实现。
>>> def keep_if(filter_fn,s)
>>> return [x for x in s if filter_fn(x)]
约定俗成的名字 (Conventional Names)。在计算机科学界,apply_to_all
更常见的名称是 map
而 keep_if
更常见的名称是 filter
。在 Python 中,内置的 map
和 filter
是这些不返回列表的函数的归纳。这些函数在第 4 章中讨论。上面的定义等效于调用内置 map
和 filter
函数的结果被应用于list
构造函数。
2.3.4 序列抽象
我们已经介绍了两种满足序列抽象的内置数据类型:列表和范围。两者都满足我们在本节开始提到的条件:长度和元素选择。Python 还包含两个扩展序列抽象的序列类型行为。
成员资格 (Membership)。值可以被测试是否属于列表。Python 有两个运算符 in
和 not in
,它们的计算结果为 True 或 False ,具体取决于元素是否出现在序列中。
切片 (Slicing)。序列中包含较小的序列。一个切片是原始序列的一段连续范围,由一对整数指定。和 range 构造函数一样,第一个整数和第二个整数分别是:范围的起始数字、范围的末尾数字再加一。
在 Python 中,序列切片的表达方式类似于元素选择,都是使用方括号。冒号分隔起始索引和结束索引。如果起始索引或结束索引被省略,则被默认是如下的极值:起始索引被省略,0 作为起始索引;结束索引被省略,序列长度作为结束索引。
字符串
成员资格 (Membership)。字符串的行为不同于 Python 中的其他序列类型。字符串抽象不同于列表和范围的完整序列抽象。尤其是,成员运算符 in
应用于字符串时,与应用于序列时相比完全不同。它匹配子字符串而不是元素。
>>> 'here' in "Where's Waldo"
True
多行文字 (Multiline Literals)。字符串不限于一行。跨越多行的字符串文字可以用三重引号括起。我们已经在文档字符串中广泛使用了这种三重引号。
字符串强制 (String Coercion) 通过以对象值作为参数调用 str
构造函数,可以从 Python 中的任何对象创建字符串。在用各种类型的对象来构造描述性字符串时,字符串的这一特性是很有用的。
2.3.6 树
列表提供了闭包的特性,再次基础上实现进一步的抽象,比较基础的是树
代码实现
def tree(root_label,branches=[]):
for branch in branches:
assert is_tree(branch), 'branches must be trees'
return [root_lable]+list(branches)
def lable(tree):
return tree[0]
def branches(tree):
return tree[1:]
判断一个列表是否为树
def is_tree(tree):
if type(tree)!=list or len(tree)<1:
return False
for branch in branches(tree):
if not is_tree(branch):
return False
return True
判断是否为叶子节点
def is_leaf(tree):
return not branches(tree)
2.3.7 链表
目前,我们只用内置类型来表示序列。但是,我们也可以开发未内置于 Python 中的序列表示。
链表 (linked list) 是一种常见的由嵌套对构造的序列表示。下面的环境图阐明了包含 1、2、3 和 4 的四元素序列的链表表示。
链表是一个 “对”,包括 first 元素(在本例中为 1)和 the rest of the sequence(在本例中为 2、3、4 的表示)。第二个元素也是一个链表。仅包含 4 的最内部链表的 the rest of the sequence 为 “empty”,“empty” 表示空链表。
链表具有递归结构:链表的其余部分是链表或 “empty”。我们可以定义一个抽象数据来代表验证、构建和选择链表的组件。