绪论

我们已经介绍过数据抽象,这是一种构造系统的方法学,它能够使程序中的大部分描述与其所操作的数据对象的具体表示无关,比如一个有理数程序的设计与有理数的实现相分离。这里的关键是构筑数据抽象屏障——在有理数的例子中即有理数的构造函数(make_rat)和获取有理数分子分母的选择函数(numerdenom)——它能将有理数的使用方式与其借助于表结构的具体表示形式隔离开。

数据抽象屏障是控制复杂性的强有力工具,然而这种类型的数据抽象还不够强大有力。从一个另一个角度看,对于一个数据对象可能存在多种有用的表示方式,且我们希望所设计的系统能够处理多种表示形式。比如,复数就可以表示为两种几乎等价的形式:直角坐标形式(实部和虚部)和极坐标形式(模和幅角)。有时采用直角坐标更方便,有时采用幅角更方便。我们希望设计的过程能够对具有任意表示形式的复数工作。

我们从简单的复数实例开始,看看如何为复数设计出直角坐标表示和极坐标表示,而又维持一种抽象的“复数”数据对象的概念。做到这一点的方式就是基于通用型选择函数(real_partimg_partmagnitudeangle)来定义复数的算术运算(add_complexsub_complexmul_complexdiv_complex),使这些选择函数能访问一个复数的各个部分,无论复数采用的是什么表示方式。采用这种方法设计的复数系统如下图所示:

上图中包含两种不同的抽象屏障。“水平”抽象屏障所扮演的角色如我们在有理数中讲的相同,他们将“高层”操作与“底层”表示隔离开。与此同时,还存在一道“垂直”屏障,它使我们能够隔离不同的设计,并且还能够安装其他的表示方式。

2.4.1 复数的表示

复数表示为有序对有两种可能表示方式:直角坐标形式(实部和虚部)和极坐标形式(模和幅角)。我们将复数集合设想为一个带有两个坐标轴(“实”轴和“虚”轴)的两维空间,如下图所示。按照这一观点,复数\(z = x + iy\)(其中\(i^2=-1\))可看作这个平面上的一个点,其中的实坐标是\(x\)而虚坐标为\(y\)。在这种表示下,复数的加法就可以归结为两个坐标相加:

\[实部 \left(z_1+z_2\right)= 实部 \left(z_1\right)+ 实部 \left(z_2\right) \\
虚部 \left(z_1+z_2\right)= 虚部 \left(z_1\right)+ 虚部 \left(z_2\right)
\]

当需要乘两个复数时,更自然的考虑是采用复数的极坐标形式,此时复数用一个模和一个幅角表示(上图中的\(r\)和\(A\))。两个复数的乘积也是一个向量,得到它的方式是模相乘,幅角相加。

\[模 \left(z_1 \cdot z_2\right)= 模 \left(z_1\right) 模 \left(z_2\right) \\
幅角 \left(z_1 \cdot z_2\right)= 幅角 \left(z_1\right)+ 幅角 \left(z_2\right)
\]

虽说复数的两种不同表示方式适合不同的运算,但从开发人员角度来看,数据抽象原理希望所有复数操作都应该可以使用,而无论计算机所用的具体表示形式是什么。例如我们常常需要取得一个复数的模,即使它原本采用的是复数的直角坐标表示;同样我们也常常需要得到复数的实部,即使它采用的是极坐标形式。

我们沿用之前有理数的设计策略,假定所有复数运算的实现都基于如下四个选择函数:real_partimg_partmagnitudeangle;还要假定有两个构造复数的过程:make_from_real_imag根据实部和虚部返回一个(基于某种表示的)复数,make_from_mag_ang根据模和幅角描述返回一个(基于某种表示的)复数。这些过程的性质是。对于任何复数\(z\)(不管其基于何种表示方式),下面两者:

make_from_real_imag(real_part(z), imag_part(z))

make_from_mag_ang(magnitude(z), angle(z))

产生出的复数都应该等于\(z\)(且保持原来的表示方式)。

我们可以利用这些构造函数和选择函数来刻画“抽象数据”,从而实现复数的算术。正如上面公式中所述,复数的加法和减法采用实部和虚部方式描述,而乘法和除法采用模和幅角的方式描述。

def add_complex(z1, z2):
return make_from_real_imag(real_part(z1) + real_part(z2), imag_part(z1) + imag_part(z2)) def sub_complex(z1, z2):
return make_from_real_imag(real_part(z1) - real_part(z2), imag_part(z1) - imag_part(z2)) def mul_complex(z1, z2):
return make_from_mag_ang(magnitude(z1) * magnitude(z2), angle(z1) + angle(z2)) def div_complex(z1, z2):
return make_from_mag_ang(magnitude(z1) / magnitude(z2), angle(z1) - angle(z2))

为了完成这一复数包,我们必须选择一种表示方式。我们假定有两个程序员Ben和Hacker。Ben选择了复数的直角坐标形式,Alyssa选择了复数的极坐标形式。对于选择直角坐标形式的Ben而言,此时实部和虚部的获取是直截了当的,但为了得到模和幅角,或需要在给定模和幅角下构造复数时,他利用了下面的三角关系:

\[\begin{array}{ll}
r=\sqrt{x^2+y^2} & A=\arctan (y, x)\\
x=r \cos A & y=r \sin A \\
\end{array}
\]

这些公式建立起实部和虚部对偶\((x, y)\)与模和幅角对偶\((r, A)\)之间的联系。Ben基于这种表示给出了下面这几个选择函数和构造函数:

import math
def real_part(z):
return z[0] def imag_part(z):
return z[1] def magnitude(z):
return math.sqrt(real_part(z) ** 2 + imag_part(z) ** 2) def angle(z):
return math.atan2(imag_part(z), real_part(z)) def make_from_real_imag(x, y):
return [x, y] def make_from_mag_ang(r, a):
return [r * math.cos(a), r * math.sin(a)]

下列是我们对Ben的直角坐标表示方法的测试结果:

complex_1 = make_from_real_imag(math.sqrt(3)/2, 1/2) # (sqrt(3)/2, 1/2)

complex_2 = make_from_real_imag(1/2, math.sqrt(3)/2) # (1/2, sqrt(3)/2)

print(add_complex(complex_1, complex_2))
# [1.3660254037844386, 1.3660254037844386], 对应(sqrt(3)/2 + 1/2, sqrt(3)/2 + 1/2)
print(mul_complex(complex_1, complex_2))
# [6.123233995736765e-17, 0.9999999999999998],对应(0, 1)

但在另一边,对于选择了复数的极坐标形式的Alyssa而言,选取模和幅角的操作直截了当,但必须通过三角关系去得到实部和虚部。Alyssa基于复数的极坐标形式所给出的选择函数和构造函数如下:

def real_part(z):
return magnitude(z) * math.cos(angle(z)) def imag_part(z):
return magnitude(z) * math.sin(angle(z)) def magnitude(z):
return z[0] def angle(z):
return z[1] def make_from_real_imag(x, y):
return [math.sqrt(x**2 + y**2), math.atan2(y, x)] def make_from_mag_ang(r, a):
return [r, a]

下列是我们对Alyssa的极坐标表示方法的测试结果:

complex_1 = make_from_mag_ang(1, math.pi/6) # (1, pi/6)

complex_2 = make_from_mag_ang(1, math.pi/3) # (1, pi/3)

print(add_complex(complex_1, complex_2))
# [1.9318516525781366, 0.7853981633974483], 对应(sqrt(6) + sqrt(2))/2, arctan(sqrt(3)/2 + 1/2, sqrt(3)/2 + 1/2))
print(mul_complex(complex_1, complex_2))
# [1, 1.5707963267948966] 对应(1, pi/2)

数据抽象的规则保证了add_complexsub_complexmul_complexdiv_complex的同一套实现对于Ben的表示或者Alyssa的表示都能正常工作。

2.4.2 带标志数据

认识数据抽象的一种方式是将其看作“最小允诺原则”的一个应用。在 2.4.1 节中我们可以选择采用Ben的直角坐标表示形式或者Alyssa的极坐标表示形式,由选择函数和构造函数形成的抽象屏障,使我们可以把为自己所用数据对象选择具体表示形式的事情尽量往后推,而且还能保持系统设计的最大灵活性。

最小允诺原则还可以推进到更极端的情况,我们可以在完成了对选择函数和构造函数的设计,并决定了同时使用Ben的表示和Alyssa的表示之后,依然维持所用表示方式的不确定性。如果要在同一个系统中包含这两种不同表示,那么就需要采用一种方式将极坐标形式的数据和直角坐标形式的数据区分开。

完成这种区分的一种方式,就是在每个复数里包含一个类型标志部分——符号rectangular或者polar,我们在操作复数时可以借助此标志来确定使用的选择函数。

为了能对带标志数据进行各种操作,我们假定有过程type_tagcontents,它们分别从数据对象中提取类型标志和实际内容(在复数的例子中即极坐标或者直角坐标)。还要假定一个过程attach_tag,它以一个标志和实际内容为参数,生成出一个带标志的数据对象。实现这些的直接方式就是采用普通的表结构:

def attach_tag(type_tag, contents):
return [type_tag, contents] def type_tag(datum):
if isinstance(datum, list):
return datum[0]
else:
raise ValueError("Bad tagged dataum -- TYPE-TAG", datum) def contents(datum):
if isinstance(datum, list):
return datum[1]
else:
raise ValueError("Bad tagged dataum -- CONTENTS", datum)

利用这些过程,我们就可以定义出谓词is_rectangularis_polar,它们分别辨识直角坐标的和极坐标的复数:

def is_rectangular(z):
return type_tag(z) == "rectangular" def is_polar(z):
return type_tag(z) == "polar"

有了类型系统之后,Ben和Alyssa现在就可以修改自己的代码,使他们的两种不同表示能共存于一个系统中了。当Ben构造一个复数时,总为它加上标志,说明采用的是直角坐标:

def real_part_rectangular(z):
return z[0] def imag_part_rectangular(z):
return z[1] import math
def magnitude_rectangular(z):
return math.sqrt(real_part_rectangular(z)**2 +
imag_part_rectangular(z)**2) def angle_rectangular(z):
return math.atan2(imag_part_rectangular(z),
real_part_rectangular(z)) def make_from_real_imag_rectangular(x, y):
return attach_tag("rectangular", [x, y]) def make_from_mag_ang_rectangular(r, a):
return attach_tag("rectangular", [r * math.cos(a), r * math.sin(a)])

Alyssa构造复数时,总将其标志设为极坐标:

def real_part_polar(z):
return magnitude_polar(z) * math.cos(angle_polar(z)) def imag_part_polar(z):
return magnitude_polar(z) * math.sin(angle_polar(z)) def magnitude_polar(z):
return z[0] def angle_polar(z):
return z[1] def make_from_real_imag_polar(x, y):
return attach_tag("polar",
[math.sqrt(x**2 + y**2),
math.atan2(y, x)]) def make_from_mag_ang_polar(r, a):
return attach_tag("polar", [r, a])

每个通用型选择函数都需要考虑到可能存在的两种复数表示情况,故它需要先检查参数的标志,然后调用处理该类数据的适当过程。例如为了得到一个复数的实部,real_part需要通过检查确定是使用Benreal_part_rectangular还是Alyssareal_part_polar。在这两种情况下,我们都用contents提取出原始的无标志数据,并将它送给所需的直角坐标过程或极坐标过程:

def real_part(z):
if is_rectangular(z):
return real_part_rectangular(contents(z))
elif is_polar(z):
return real_part_polar(contents(z))
else:
raise ValueError("Unknown type -- REAL-PART", z) def imag_part(z):
if is_rectangular(z):
return imag_part_rectangular(contents(z))
elif is_polar(z):
return imag_part_polar(contents(z))
else:
raise ValueError("Unknown type -- IMAG-PART", z) def magnitude(z):
if is_rectangular(z):
return magnitude_rectangular(contents(z))
elif is_polar(z):
return magnitude_polar(contents(z))
else:
raise ValueError("Unknown type -- MAGNITUDE", z) def angle(z):
if is_rectangular(z):
return angle_rectangular(contents(z))
elif is_polar(z):
return angle_polar(contents(z))
else:
raise ValueError("Unknown type -- ANGLE", z)

在实现复数算术运算时,我们仍然可以采用取自2.4.1节的同样过程add_complexsub_complexmul_complexdiv_complex,因为它们所调用的选择函数都是通用型的,对任何表示都能工作,例如过程add_complex仍然是:

def add_complex(z1, z2):
return make_from_real_imag(real_part(z1) + real_part(z2),
imag_part(z1) + imag_part(z2))

最后,我们还必须选择是采用Ben的表示还是Alyssa的表示构造复数。一种合理的选择是,手头有实部和虚部时,构造函数的返回采用直角坐标表示;手头有模和幅角时,构造函数的返回采用极坐标表示:

def make_from_real_imag(x, y):
# 手头有实部和虚部时,构造函数的返回采用直角坐标表示
return make_from_real_imag_rectangular(x, y) def make_from_mag_angle(r, a):
# 手头有模和幅角时,构造函数的返回采用极坐标表示
return make_from_mag_ang_polar(r, a)

下面是我们对这样的复数系统进行的测试结果:

complex_1  = make_from_mag_ang_polar(1, math.pi/6) # (1, pi/6)
complex_2 = make_from_mag_ang_polar(1, math.pi/3) # (1, pi/3) print(add_complex(complex_1, complex_2))
# ['rectangular', [1.3660254037844388, 1.3660254037844386]], 对应(sqrt(3)/2 + 1/2, sqrt(3)/2 + 1/2)

实际上,这样得到的复数系统所具有的结构如下所示:

可见这一系统已经分解为三个相对独立的部分:复数算术运算、Alyssa的极坐标实现和Ben的直角坐标实现。极坐标或直角坐标的实现可以是Ben和Alyssa独立工作写成的东西,这两部分又被第三个程序员作为基础表示,用于在抽象构造函数和选择函数的界面(interface)之上实现各种复数算术过程。

2.4.3 数据导向的程序设计和可加性

检查一个数据项的类型,并据此去调用某个适当过程称为基于类型的分派。在系统设计中,这是一种获得模块性的强有力策略。而在另一方面,像2.4.2节那样实现的分派有两个明显的弱点。第一个弱点是,其中的这些通用型界面过程(real_partimag_partmagnitudeangle)必须知道所有的不同表示。举例来说,假定现在希望为前面的复数系统增加一种表示,我们就必须将这一新表示方式标识为一种新类型,而且要在每个通用界面过程里增加一个子句,检查这一新类型。

第二个弱点是,即使这些独立的表示形式可以分别设计,我们也必须保证在系统里不存在两个名字相同的过程。正因如此,Ben和Alyssa必须去修改原来在2.4.1节中给出的那些过程的名字。

位于这两个弱点之下的基础问题是,上面这种实现通用型界面的技术不具有可加性。在每次增加一种新表示形式时,使用通用选择函数的人都必须修改他们的过程,而那些做独立表示的界面的人也必须修改代码以避免名字冲突问题,这非常不方便,且容易引进错误。

现在我们需要的是一种能够将系统设计进一步模块化的方法。一种称为数据导向的程序设计的编程技术提供了这种能力。在这种编程技术中,我们可以将处理针对不同类型的一些公共通用操作视为处理一个二维表格,其中一维包含着所有的可能操作,另一维就是所有的可能类型。在前一节开发的复数系统里,我们也可以将同样的信息组织为一个表格,如下图所示:

在我们上面所实现的复数系统中,采用的方式是用一些过程做为复数算术与两个表示包之间的界面,并且让这些过程中的每一个去做基于类型的显式分派。而数据导向的程序设计则意味着我们可以把这一界面实现为一个过程,让它用操作名和参数类型的组合到表格中查找,以便找出应该调用的适当过程。

参考

  • [1] Abelson H, Sussman G J. Structure and interpretation of computer programs[M]. The MIT Press, 1996.

SICP:复数的直角和极坐标的表示(Python实现)的更多相关文章

  1. 第 16 章 C 预处理器和 C 库(直角坐标转换极坐标)

    /*------------------------------------- rect_pol.c -- 把直角坐标转换为极坐标 ---------------------------------- ...

  2. 复数基础及其2D空间的旋转

    本文我们讨论复数及其旋转的含义.复数很有意思,本文介绍了复数的基本定义和性质,以及它关于旋转的几何意义. 复数对于旋转的两个方面极为重要: 1. 它引入了旋转算子(rotational operato ...

  3. python里的复数complex

    复数是一个数学概念,包含了实部和虚部.在python设计语言中,可以直接定义以j为单位,也可以使用complex函数创建复数,这个函数可以传实部和虚部,也可以只传实部. 我们把形如z=a+bj(a,b ...

  4. SICP 课程总结 & 复习

    SICP 课程总结 & 复习 小作文 有赖于那个终极的.伟大的.命定的教务系统,我选上了这门课:SICP,Structure and Interpret of Computer Program ...

  5. 《University Calculus》-chape8-无穷序列和无穷级数-欧拉恒等式

    写在前面:写在前面的当然是对大天朝教材的吐槽啦. 曾记否,高中所学虚数和复平面的概念,如此虚无的概念到了大学一门叫<模拟电子技术>的课程中居然明目张胆的开始进行计算! 曾记否,高中的指对运 ...

  6. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第二十二章:四元数(QUATERNIONS) 学习目标 回顾复数,以及 ...

  7. 论文翻译:2022_PACDNN: A phase-aware composite deep neural network for speech enhancement

    论文地址:PACDNN:一种用于语音增强的相位感知复合深度神经网络 引用格式:Hasannezhad M,Yu H,Zhu W P,et al. PACDNN: A phase-aware compo ...

  8. 论文翻译:2020_DCCRN: Deep Complex Convolution Recurrent Network for Phase-Aware Speech Enhancement

    论文地址:DCCRN:用于相位感知语音增强的深度复杂卷积循环网络 论文代码:https://paperswithcode.com/paper/dccrn-deep-complex-convolutio ...

  9. 1、Python基础知识

    输出print “houkai”,3.0版本后print修改为函数,print(‘houkai’) 数学运算:默认整数整除1/2=0而1.0/2=0.5,可以使用from __future__ imp ...

  10. Python学习日志(一)

    Python的安装 访问http://www.python.org 点击downloads,选择Windows 我在这里选择了Latest Python 3 Release - Python 3.5. ...

随机推荐

  1. Spring中过滤器(Filter)和拦截器(Interceptor)的区别和联系

    在我们日常的开发中,我们经常会用到Filter和Interceptor.有时同一个功能.Filter可以做,Interceptor也可以做.有时就需要考虑使用哪一个比较好.这篇文章主要介绍一下,二者的 ...

  2. 使用jmx exporter采集kafka指标

    预置条件 安装kafka.prometheus 使用JMX exporter暴露指标 下载jmx exporter以及配置文件.Jmx exporter中包含了kafka各个组件的指标,如server ...

  3. pod(八):pod的调度——将 Pod 指派给节点

    目录 一.系统环境 二.前言 三.pod的调度 3.1 pod的调度概述 3.2 pod自动调度 3.2.1 创建3个主机端口为80的pod 3.3 使用nodeName 字段指定pod运行在哪个节点 ...

  4. Arch Linux + KDE 配置&美化(持续更新~)

    Arch Linux + KDE 配置&美化(持续更新~) 这篇文章着重记录archlinux + KDE的一个基本的配置过程.不包括安装过程(使用archInstall.sh).内容大概有以 ...

  5. 【操作说明】全能型H.265播放器如何使用?

    本播放器集成了公司业务的接口,包含了实播,回放,云台控制和回放速度控制,截图和全屏功能可以根据type直接初始化接口地址如果是第三方业务对接,也可以单独配置接口地址 正确使用H.265播放器需要按以下 ...

  6. 【ELK解决方案】ELK集群+RabbitMQ部署方案以及快速开发RabbitMQ生产者与消费者基础服务

    前言: 大概一年多前写过一个部署ELK系列的博客文章,前不久刚好在部署一个ELK的解决方案,我顺便就把一些基础的部分拎出来,再整合成一期文章.大概内容包括:搭建ELK集群,以及写一个简单的MQ服务. ...

  7. vue引用MarkDown(mavonEditor)编辑器,文档

    mavonEditor Install mavon-editor (安装) npm install mavon-editor --save 如何引入: 全局引用: // 全局注册 import Vue ...

  8. 安装kali linux(干货)

    安装kali 一. 准备工具 1. VMware Workstation Pro https://www.vmware.com/cn/products/workstation-pro/workstat ...

  9. 3 c++编程-提高篇-模版

    ​ 重新系统学习c++语言,并将学习过程中的知识在这里抄录.总结.沉淀.同时希望对刷到的朋友有所帮助,一起加油哦!  生命就像一朵花,要拼尽全力绽放!死磕自个儿,身心愉悦!  系列文章列表: 1 c+ ...

  10. 在C#中使用Halcon开发视觉检测程序

    目录 简介 将 HALCON/.NET 添加到应用程序 添加控件 引用dll 调用Halcon算子 程序示例 HSmartWindowControl控件使用 加载.保存图像 扩展:加载相机图像 画线. ...