伯克利CS 61A的教学用书笔记和一些课程笔记
The general technique of isolating the parts of a program that deal with how data are represented from the parts that deal with how data are manipulated is a powerful design methodology called data abstraction. Data abstraction makes programs much easier to design, maintain, and modify. 将数据的表示和数据的操作分离开的一种抽象方法,称为数据抽象。
它将如何使用复合数据与如何构造复合数据隔离开,尽可能地满足通用性,
2.2.1 Example: Rational Numbers
有理数都可以表示成分数的形式:<numerator>/<denominator>。<numerator> 和 <denominator>都是整数。
对于浮点数,很多时候会出现误差,不能直接进行相等判断等操作,比如
>>> 1/3
0.3333333333333333
>>> 1/3 == 0.333333333333333300000 # Dividing integers yields an approximation
True
所以可以考虑用分数来精确表示。下面进行数据抽象。
有理数的值就是由分子和分母决定的,先假设我们已经有方法将一个有理数构造为分子和分母两部分,也有方法获取一个有理数的分子或者分母:
- rational(n, d) 基于n和d返回有理数
- numer(x) 返回有理数x的分子
- denom(x) 返回有理数x的分母
We are using here a powerful strategy for designing programs: wishful thinking. 一种设计程序的强有力的策略:wishful thinking。先不考虑以上三个函数是怎么实现的,甚至不考虑有理数具体通过程序应该怎么表示。
我们先考虑有理数的操作,加法、乘法、打印、相等判断:
>>> def add_rationals(x, y):
nx, dx = numer(x), denom(x)
ny, dy = numer(y), denom(y)
return rational(nx * dy + ny * dx, dx * dy)
>>> def mul_rationals(x, y):
return rational(numer(x) * numer(y), denom(x) * denom(y))
>>> def print_rational(x):
print(numer(x), '/', denom(x))
>>> def rationals_are_equal(x, y):
return numer(x) * denom(y) == numer(y) * denom(x)
现在我们基于rational,numer,denom三个函数,实现了对有理数的处理,接下来就是实现这三个函数,进一步思考,我们需要的是将分子和分母整合成为一个复合值的方法。
2.2.2 Pairs
python提供了一种叫列表的复合结构,可以用来形成复合对。
>>> pair = [10, 20]
>>> pair
[10, 20]
>>> x, y = pair
>>> x
10
>>> y
20
>>> pair[0]
10
>>> pair[1]
20
>>> from operator import getitem
>>> getitem(pair, 0)
10
>>> getitem(pair, 1)
20
两元素列表不是python中表示pairs的唯一方式,利用列表,我们就可以将分子和分母整合到一起,用来表示有理数。
>>> def rational(n, d):
return [n, d]
>>> def numer(x):
return x[0]
>>> def denom(x):
return x[1]
现在,我们就可以进行计算了。
>>> half = rational(1, 2)
>>> print_rational(half)
1 / 2
>>> third = rational(1, 3)
>>> print_rational(mul_rationals(half, third))
1 / 6
>>> print_rational(add_rationals(third, third))
6 / 9
但是我们的方法仍然有缺陷,比如最后的6/9并没有化到最简。我们还需要将分子分母除以他们的最大公约数。
python库的gcd函数能求出两个数的最大公约数。
>>> from fractions import gcd
>>> def rational(n, d):
g = gcd(n, d)
return (n//g, d//g)
然后,我们完成了对有理数的抽象。我们只改变了构造有理数的函数,而不需要改动处理有理数的函数。
>>> print_rational(add_rationals(third, third))
2 / 3
2.2.3 Abstraction Barriers
抽象壁垒。我们在上面的例子中可以看到,我们对数据的构造表示与数据的处理进行了隔离,我们操作数据时不需要关心数据是怎么构造的,就像我们操作list的时候不关心list在python中具体是怎么实现的一样。
Parts of the program that... | Treat rationals as... | Using only... |
---|---|---|
Use rational numbers to perform computation | whole data values | add_rational, mul_rational, rationals_are_equal, print_rational |
Create rationals or implement rational operations | numerators and denominators | rational, numer, denom |
Implement selectors and constructor for rationals | two-element lists | list literals and element selection |
如上表,我们有三层隔离,第一层,我们只关心怎么用有理数,将有理数当作一个整体,而不关心有理数具体是怎么构造的,第二层,我们只关心有理数具体是构造的,将有理数视为分子和分母,我们怎么用这样的构造方法来实现对有理数的操作,第三层,我么只关心怎么使用list来构造有理数,不关心python的list是怎么具体实现的。在上面的每一层中,最后一列中的函数执行一个抽象屏障。这些函数由较高层调用,并使用较低层抽象实现。
An abstraction barrier violation occurs whenever a part of the program that can use a higher level function instead uses a function in a lower level. 壁垒冲突
比如,要实现计算有理数x的平方,最好使用已经实现了的mul_rational函数,因为这样就不涉及任何有理数的实现细节,也不会对有理数的构造带来更多假设。
>>> def square_rational(x):
return mul_rational(x, x)
再比如,直接引用有理数的分子分母也会打破一个抽象隔离。
>>> def square_rational_violating_once(x):
return rational(numer(x) * numer(x), denom(x) * denom(x))
再比如,直接用list实现,更会造成两次对抽象隔离的打破。
>>> def square_rational_violating_twice(x):
return [x[0] * x[0], x[1] * x[1]]
抽象隔离使得程序更容易维护和修改,因为当程序依赖的特定表示的函数越少,那么想要更改该表示时需要的变动就越少,函数抽象程度、通用程度越高,越容易维护,上面三个例子对有理数x平方的实现,运行结果都是对的,但是只有第一个的实现对未来来说是健壮的,比如,numer和denom的函数名改变时,那么第二个函数也需要更改,而第一个函数不需要,如果对有理数的构造改变时,第三个函数就有可能需要更改,而第一个可以通过适当的编写方式而不用更改。
2.2.4 The Properties of Data
In general, we can express abstract data using a collection of selectors and constructors, together with some behavior conditions. As long as the behavior conditions are met (such as the division property above), the selectors and constructors constitute a valid representation of a kind of data. The implementation details below an abstraction barrier may change, but if the behavior does not, then the data abstraction remains valid, and any program written using this data abstraction will remain correct. 由于抽象隔离的存在,当用来表示抽象数据的表达方式、行为条件没有改变时,抽象数据的实现细节就算改变了,这个数据抽象往往仍然是有效的,而任何使用这种抽象数据的程序都是正确的。比如对于将两个整数构造为一个有理数的函数rational(n, d),是以list为基础实现的,但是就算不是用list,是用其他可以将n,d整合的数据类型,也同样可以实现,整个程序的其他地方也不需要改变,前文也提到list并不是实现rational的唯一方式。
下面我们不用list,自己实现一个将整数整合为一个有理数的函数。
- 要求pair输入为两个整数pair(x, y),而且 select(p, 0) 返回x,而且 select(p, 1) 返回y
>>> def pair(x, y):
"""Return a function that represents a pair."""
def get(index):
if index == 0:
return x
elif index == 1:
return y
return get
>>> def select(p, i):
"""Return the element at index i of pair p."""
return p(i)
>>> p = pair(20, 14)
>>> select(p, 0)
20
>>> select(p, 1)
14
虽然这些高阶函数(higher-order function)完全不符合我们对有理数的直观概念,但是它是足以表示有理数,我们实现这个例子并不是说python实际上就是这么做的,而是说我们可以这么做,不依靠list,我们可以用很多方式实现对有理数的构造。
所以,对数据的抽象可以让我们轻松的在实现之间切换。