jjzjj

SICP:复数的直角和极坐标的表示(Python实现)

Orion's Blog 2023-03-28 原文

绪论

我们已经介绍过数据抽象,这是一种构造系统的方法学,它能够使程序中的大部分描述与其所操作的数据对象的具体表示无关,比如一个有理数程序的设计与有理数的实现相分离。这里的关键是构筑数据抽象屏障——在有理数的例子中即有理数的构造函数(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. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  2. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  3. Python 相当于 Perl/Ruby ||= - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:Pythonconditionalassignmentoperator对于这样一个简单的问题表示歉意,但是谷歌搜索||=并不是很有帮助;)Python中是否有与Ruby和Perl中的||=语句等效的语句?例如:foo="hey"foo||="what"#assignfooifit'sundefined#fooisstill"hey"bar||="yeah"#baris"yeah"另外,类似这样的东西的通用术语是什么?条件分配是我的第一个猜测,但Wikipediapage跟我想的不太一样。

  4. java - 什么相当于 ruby​​ 的 rack 或 python 的 Java wsgi? - 2

    什么是ruby​​的rack或python的Java的wsgi?还有一个路由库。 最佳答案 来自Python标准PEP333:Bycontrast,althoughJavahasjustasmanywebapplicationframeworksavailable,Java's"servlet"APImakesitpossibleforapplicationswrittenwithanyJavawebapplicationframeworktoruninanywebserverthatsupportstheservletAPI.ht

  5. ruby-on-rails - 复数 for fields_for has_many 关联未显示在 View 中 - 2

    目前,Itembelongs_toCompany和has_manyItemVariants。我正在尝试使用嵌套的fields_for通过Item表单添加ItemVariant字段,但是使用:item_variants不显示该表单。只有当我使用单数时才会显示。我检查了我的关联,它们似乎是正确的,这可能与嵌套在公司下的项目有关,还是我遗漏了其他东西?提前致谢。注意:下面的代码片段中省略了不相关的代码。编辑:不知道这是否相关,但我正在使用CanCan进行身份验证。routes.rbresources:companiesdoresources:itemsenditem.rbclassItemi

  6. Ruby - 如何将消息长度表示为 2 个二进制字节 - 2

    我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi

  7. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  8. python - 如何读取 MIDI 文件、更改其乐器并将其写回? - 2

    我想解析一个已经存在的.mid文件,改变它的乐器,例如从“acousticgrandpiano”到“violin”,然后将它保存回去或作为另一个.mid文件。根据我在文档中看到的内容,该乐器通过program_change或patch_change指令进行了更改,但我找不到任何在已经存在的MIDI文件中执行此操作的库.他们似乎都只支持从头开始创建的MIDI文件。 最佳答案 MIDIpackage会为您完成此操作,但具体方法取决于midi文件的原始内容。一个MIDI文件由一个或多个音轨组成,每个音轨是十六个channel中任何一个上的

  9. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  10. 「Python|Selenium|场景案例」如何定位iframe中的元素? - 2

    本文主要介绍在使用Selenium进行自动化测试或者任务时,对于使用了iframe的页面,如何定位iframe中的元素文章目录场景描述解决方案具体代码场景描述当我们在使用Selenium进行自动化测试的时候,可能会遇到一些界面或者窗体是使用HTML的iframe标签进行承载的。对于iframe中的标签,如果直接查找是无法找到的,会抛出没有找到元素的异常。比如近在咫尺的例子就是,CSDN的登录窗体就是使用的iframe,大家可以尝试通过F12开发者模式查看到的tag_name,class_name,id或者xpath来定位中的页面元素,会抛出NoSuchElementException异常。解决

随机推荐