SICP Python 描述 2.2 数据抽象

2.2 数据抽象

来源:2.2 Data Abstractionhtml

译者:飞龙python

协议:CC BY-NC-SA 4.0git

因为咱们但愿在程序中表达世界中的大量事物,咱们发现它们的大多数都具备复合结构。日期是年月日,地理位置是精度和纬度。为了表示位置,咱们但愿程序语言具备将精度和纬度“粘合”为一对数据的能力 -- 也就是一个复合数据结构 -- 使咱们的程序可以以一种方式操做数据,将位置看作单个概念单元,它拥有两个部分。github

复合数据的使用也让咱们增长程序的模块性。若是咱们能够直接将地理位置看作对象来操做,咱们就能够将程序的各个部分分离,它们根据这些值如何表示来从本质上处理这些值。将某个部分从程序中分离的通常技巧是一种叫作数据抽象的强大的设计方法论。这个部分用于处理数据表示,而程序用于操做数据。数据抽象使程序更易于设计、维护和修改。编程

数据抽象的特征相似于函数抽象。当咱们建立函数抽象时,函数如何实现的细节被隐藏了,并且特定的函数自己能够被任何具备相同行为的函数替换。换句话说,咱们能够构造抽象来使函数的使用方式和函数的实现细节分离。与之类似,数据抽象是一种方法论,使咱们将复合数据对象的使用细节与它的构造方式隔离。数据结构

数据抽象的基本概念是构造操做抽象数据的程序。也就是说,咱们的程序应该以一种方式来使用数据,对数据作出尽量少的假设。同时,须要定义具体的数据表示,独立于使用数据的程序。咱们系统中这两部分的接口是一系列函数,叫作选择器和构造器,它们基于具体表示实现了抽象数据。为了演示这个技巧,咱们须要考虑如何设计一系列函数来操做有理数。app

当你阅读下一节时,要记住当今编写的多数 Python 代码使用了很是高级的抽象数据类型,它们内建于语言中,好比类、字典和列表。因为咱们正在了解这些抽象的工做原理,咱们本身不能使用它们。因此,咱们会编写一些不那么 Python 化的代码 -- 它并非在语言中实现咱们的概念的一般方式。可是,咱们所编写的代码出于教育目的,它展现了这些抽象如何构建。要记住计算机科学并不仅是学习如何使用编程语言,也学习它们的工做原理。编程语言

2.2.1 示例:有理数的算术

有理数可表示为整数的比值,而且它组成了实数的一个重要子类。相似于1/3或者17/29的有理数一般可编写为:函数

<numerator>/<denominator>

其中,<numerator><denominator>都是值为整数的占位符。有理数的值须要两部分来描述。工具

有理数在计算机科学中很重要,由于它们就像整数那样,能够准确表示。无理数(好比pi 或者 e 或者 sqrt(2))会使用有限的二元展开代替为近似值。因此在原则上,有理数的处理应该让咱们避免算术中的近似偏差。

可是,一旦咱们真正将分子与分母相除,咱们就会只剩下截断的小数近似值:

>>> 1/3
0.3333333333333333

当咱们开始执行测试时,这个近似值的问题就会出现:

>>> 1/3 == 0.333333333333333300000  # Beware of approximations
True

计算机如何将实数近似为定长的小数扩展,是另外一门课的话题。这里的重要概念是,经过将有理数表示为整数的比值,咱们可以彻底避免近似问题。因此出于精确,咱们但愿将分子和分母分离,可是将它们看作一个单元。

咱们从函数抽象中了解到,咱们能够在了解某些部分的实现以前开始编出东西来。让咱们一开始假设咱们已经拥有一种从分子和分母中构造有理数的方式。咱们也假设,给定一个有理数,咱们都有办法来提取(或选中)它的分子和分母。让咱们进一步假设,构造器和选择器如下面三个函数来提供:

  • make_rat(n, d)返回分子为n和分母为d的有理数。

  • numer(x)返回有理数x的分子。

  • denom(x)返回有理数x的分母。

咱们在这里正在使用一个强大的合成策略:心想事成。咱们并无说有理数如何表示,或者numerdenommake_rat如何实现。即便这样,若是咱们拥有了这三个函数,咱们就能够执行加法、乘法,以及测试有理数的相等性,经过调用它们:

>>> def add_rat(x, y):
        nx, dx = numer(x), denom(x)
        ny, dy = numer(y), denom(y)
        return make_rat(nx * dy + ny * dx, dx * dy)
>>> def mul_rat(x, y):
        return make_rat(numer(x) * numer(y), denom(x) * denom(y))
>>> def eq_rat(x, y):
        return numer(x) * denom(y) == numer(y) * denom(x)

如今咱们拥有了由选择器函数numerdenom,以及构造器函数make_rat定义的有理数操做。可是咱们尚未定义这些函数。咱们须要以某种方式来将分子和分母粘合为一个单元。

2.2.2 元组

为了实现咱们的数据抽象的具体层面,Python 提供了一种复合数据结构叫作tuple,它能够由逗号分隔的值来构造。虽然并非严格要求,圆括号一般在元组周围。

>>> (1, 2)
(1, 2)

元组的元素能够由两种方式解构。第一种是咱们熟悉的多重赋值:

>>> pair = (1, 2)
>>> pair
(1, 2)
>>> x, y = pair
>>> x
1
>>> y
2

实际上,多重赋值的本质是建立和解构元组。

访问元组元素的第二种方式是经过下标运算符,写做方括号:

>>> pair[0]
1
>>> pair[1]
2

Python 中的元组(以及多数其它编程语言中的序列)下标都以 0 开始,也就是说,下标 0 表示第一个元素,下标 1 表示第二个元素,以此类推。咱们对这个下标惯例的直觉是,下标表示一个元素距离元组开头有多远。

与元素选择操做等价的函数叫作getitem,它也使用下标以 0 开始的位置来在元组中选择元素。

元素是原始类型,也就是说 Python 的内建运算符能够操做它们。咱们不久以后再来看元素的完整特性。如今,咱们只对元组如何做为胶水来实现抽象数据类型感兴趣。

表示有理数。元素提供了一个天然的方式来将有理数实现为一对整数:分子和分母。咱们能够经过操做二元组来实现咱们的有理数构造器和选择器函数。

>>> def make_rat(n, d):
        return (n, d)
>>> def numer(x):
        return getitem(x, 0)
>>> def denom(x):
        return getitem(x, 1)

用于打印有理数的函数完成了咱们对抽象数据结构的实现。

>>> def str_rat(x):
        """Return a string 'n/d' for numerator n and denominator d."""
        return '{0}/{1}'.format(numer(x), denom(x))

将它与咱们以前定义的算术运算放在一块儿,咱们可使用咱们定义的函数来操做有理数了。

>>> half = make_rat(1, 2)
>>> str_rat(half)
'1/2'
>>> third = make_rat(1, 3)
>>> str_rat(mul_rat(half, third))
'1/6'
>>> str_rat(add_rat(third, third))
'6/9'

就像最后的例子所展现的那样,咱们的有理数实现并无将有理数化为最简。咱们能够经过修改make_rat来补救。若是咱们拥有用于计算两个整数的最大公约数的函数,咱们能够在构造一对整数以前将分子和分母化为最简。这可使用许多实用工具,例如 Python 库中的现存函数。

>>> from fractions import gcd
>>> def make_rat(n, d):
        g = gcd(n, d)
        return (n//g, d//g)

双斜杠运算符//表示整数除法,它会向下取整除法结果的小数部分。因为咱们知道g能整除nd,整数除法正好适用于这里。如今咱们的

>>> str_rat(add_rat(third, third))
'2/3'

符合要求。这个修改只经过修改构造器来完成,并无修改任何实现实际算术运算的函数。

扩展阅读。上面的str_rat实现使用了格式化字符串,它包含了值的占位符。如何使用格式化字符串和format方法的细节请见 Dive Into Python 3 的格式化字符串一节。

2.2.3 抽象界限

在以更多复合数据和数据抽象的例子继续以前,让咱们思考一些由有理数示例产生的问题。咱们使用构造器make_rat和选择器numerdenom定义了操做。一般,数据抽象的底层概念是,基于某个值的类型的操做如何表达,为这个值的类型肯定一组基本的操做。以后使用这些操做来操做数据。

咱们能够将有理数系统想象为一系列层级。

平行线表示隔离系统不一样层级的界限。每一层上,界限分离了使用数据抽象的函数(上面)和实现数据抽象的函数(下面)。使用有理数的程序仅仅经过算术函数来操做它们:add_ratmul_rateq_rat。相应地,这些函数仅仅由构造器和选择器make_ratnumerand denom来实现,它们自己由元组实现。元组如何实现的字节和其它层级没有关系,只要元组支持选择器和构造器的实现。

每一层上,盒子中的函数强制划分了抽象的边界,由于它们仅仅依赖于上层的表现(经过使用)和底层的实现(经过定义)。这样,抽象界限能够表现为一系列函数。

抽象界限具备许多好处。一个好处就是,它们使程序更易于维护和修改。不多的函数依赖于特定的表现,当一我的但愿修改表现时,不须要作不少修改。

2.2.4 数据属性

咱们经过实现算术运算来开始实现有理数,实现为这三个非特定函数:make_ratnumerdenom。这里,咱们能够认为已经定义了数据对象 -- 分子、分母和有理数 -- 上的运算,它们的行为由这三个函数规定。

可是数据意味着什么?咱们还不能说“提供的选择器和构造器实现了任何东西”。咱们须要保证这些函数一块儿规定了正确的行为。也就是说,若是咱们从整数nd中构造了有理数x,那么numer(x)/denom(x)应该等于n/d

一般,咱们能够将抽象数据类型当作一些选择器和构造器的集合,并带有一些行为条件。只要知足了行为条件(好比上面的除法特性),这些函数就组成了数据类型的有效表示。

这个观点能够用在其余数据类型上,例如咱们为实现有理数而使用的二元组。咱们实际上不会谈论元组是什么,而是谈论由语言提供的,用于操做和建立元组的运算符。咱们如今能够描述二元组的行为条件,二元组一般叫作偶对,在表示有理数的问题中有所涉及。

为了实现有理数,咱们须要一种两个整数的粘合形式,它具备下列行为:

  • 若是一个偶对pxy构造,那么getitem_pair(p, 0)返回xgetitem_pair(p, 1)返回y

咱们能够实现make_pairgetitem_pair,它们和元组同样知足这个描述:

>>> def make_pair(x, y):
        """Return a function that behaves like a pair."""
        def dispatch(m):
            if m == 0:
                return x
            elif m == 1:
                return y
        return dispatch
>>> def getitem_pair(p, i):
        """Return the element at index i of pair p."""
        return p(i)

使用这个实现,咱们能够建立和操做偶对:

>>> p = make_pair(1, 2)
>>> getitem_pair(p, 0)
1
>>> getitem_pair(p, 1)
2

这个函数的用法不一样于任何直观上的,数据应该是什么的概念。并且,这些函数知足于在咱们的程序中表示复合数据。

须要注意的微妙的一点是,由make_pair返回的值是叫作dispatch的函数,它接受参数m并返回xy。以后,getitem_pair调用了这个函数来获取合适的值。咱们在这一章中会屡次返回拆解这个函数的话题。

这个偶对的函数表示并非 Python 实际的工做机制(元组实现得更直接,出于性能因素),可是它能够以这种方式工做。这个函数表示虽然不是很明显,可是是一种足够完美来表示偶对的方式,由于它知足了偶对惟一须要知足的条件。这个例子也代表,将函数当作值来操做的能力,提供给咱们表示复合数据的能力。

相关文章
相关标签/搜索