书名:线性代数与Python解法
ISBN:978-7-115-60669-3
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
编 著 徐子珊
主 审 刘新旺
责任编辑 张 涛
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书共5章:第1章介绍代数系统的基本概念, 内容包括集合与映射、群、环、域及线性代数系统等; 第2章介绍矩阵代数, 内容包括矩阵定义、矩阵的各种运算, 如线性运算、乘法、转置、方阵的行列式等, 并由此讨论可逆阵的概念及性质; 第3章介绍线性方程组的消元法, 为后面讲解向量空间的知识奠定基础; 第4章基于矩阵、线性方程组等讨论应用广泛的向量空间, 内容包括向量及其线性运算、向量组的线性相关性、线性空间的线性变换等; 在以上几章的基础上, 第5章定义向量的内积运算, 在向量空间中引入“度量”, 即向量的长度(范数), 从而将二维、三维的几何空间扩展到一般的n维欧几里得空间.
本书选择Python的科学计算的软件包NumPy作为计算工具, 针对书中讨论的线性代数的计算问题给出详尽的Python解法. 本书中的每一段程序都给出了详尽的注释及说明, 适合各层次读者阅读.
本书以“代数系统”为引领,以“演绎”的方式探讨线性代数的理论与方法.第1章介 绍“代数系统”概念——定义了若干运算的集合,以及“经典代数系统”,即群、环、域.以 此为起点引入“线性代数系统”,即在一个加法交换群上,添加群中元素与数域中数的乘法 运算而得的代数系统——加法运算和数乘法运算统称为线性运算.线性代数系统是很多自 然系统与人工系统的数学模型.最经典的线性代数系统之一是第2章详尽讨论的“矩阵代 数”,其核心是同形矩阵(具有相同行数、列数的矩阵)集合上的加法和数乘法构成的一个 线性代数系统.然而,矩阵集合仅作为线性代数系统,在实际应用上是不够的.在引入矩阵 的“乘法”运算后,矩阵代数就成为描述各种问题的重要数学模型.矩阵代数最重要的应用 领域之一是本书第3章介绍的线性方程组.基于矩阵的各种运算及性质,第3章不仅给出 线性方程组的解法,而且给出了确定方程组的有解条件、解集的结构等重要的结论.这些结 论为更深入地探讨第4章中的向量代数系统提供了强有力的计算方法.向量空间不仅是二 维平面和三维立体的理论拓展,还可用以描述现实问题.例如,对第2章引入的矩阵,我 们可更细致地把矩阵拆解成行向量或列向量,矩阵的线性运算平移到了向量上,矩阵的乘 法拆解成了行向量与列向量的“内积”;于是,线性方程组的矩阵形式被拆解成了向量形式, 进而成了研究向量间线性关系的“利器”.由于引入了同形向量的内积运算,使得高维向量 空间与我们看到的二维平面和三维立体一样有了 “几何形象”:向量有“长度”——范数、 向量间有夹角,这是本书第5章讨论欧几里得空间的有趣内容.总之,本书以发展的观点 讨论线性代数:向量(从同构的视角看,矩阵亦可被视为向量)由最核心的交换群增加数乘 法后构成线性代数系统、加入内积运算后构成欧几里得空间……这未尝不是我们构建人造 系统的一种思想方法:把问题涉及的对象视为集合,从最基本的处理(运算)方法开始,逐 步添加所需的处理(运算)方法,使系统日臻完善.
华罗庚先生在《高等数学引论》的序言中写道:“我讲书喜欢埋些伏笔,把有些重要的 概念、重要的方法尽可能早地在具体问题中提出,并且不止一次地提出.”先生的意思是学 习者能在书中不同地方逐步体会到这些重要概念、方法的精妙之处.笔者在本书的写作过 程中也试着仿照先生的做法:将重要的结论拆分成若干个引理、定理和推论;一些重要但 较简单的概念在例题和练习题中提出,让读者在稍后阅读到这些内容时发现这些概念的关 键作用.在快节奏的现代社会,读者的时间非常宝贵,为此,笔者将一些定理(包括引理、 推论)的比较复杂的理论证明以“本章附录”的形式,放在每章(除第1章夕卜)的末尾,待 读者空闲时仔细研读.这样的内容安排既可以让读者流畅地阅读,同时又可以帮助读者深 入理解、掌握理论的来龙去脉(对于数学书,笔者极力主张弄懂知识的“来龙去脉”的学习 方法).例题和练习题是理工科图书的作者与读者思想交互的重要“桥梁”,本书共提供了 145道例题、107道练习题.每道练习题都由其之前的例题作为引导,读者可参考相关例题, 顺利完成练习题的解答.此外,每道计算型的练习题均有参考答案,可供读者快速检验自己 的解题结果.
近年来,Python及其数学包“异军突起”,除了因为其开放代码资源,还因为其代码 可直接被嵌入智能系统.本书用Python的科学计算的软件包NumPy来求解书中的所有 问题;选择Jupyter Notebook作为程序编写和运行平台,Jupyter Notebook的使用界面与 MATLAB十分接近,非常适合用来做科学计算.本书的所有程序都经过精心调试,并有详尽 的注释及说明.为方便读者学习,程序以chapterxx.ipynb(xx表示章的序号)的形式命名. 读者可先启动Jupyter Notebook,然后打开对应文件,调试、运行各个程序.所有的.ipynb 文件和自编的通用函数文件utility.py都保存在笔者的Gitee账号https://gitee.com/xu- zishan下的Algebra-with-Python文件夹内,读者可自行下载并使用这些文件.笔者开通了 博客(博客网址是https://blog.csdn.net/u012958850),并将持续维护、更新博文,还会在 博客中添加新的例题以及本书的勘误信息,欢迎读者通过博客与笔者沟通、交流.
本书在描述线性代数的基本理论与方法时,尽量采用目前国内外大多数教材上的通用 表述法,以便读者阅读.为了与程序代码中的常数“0”进行区分,本书将零向量及仅含一 列零或一行零的矩阵用粗斜体小写字母“o”表示,将一般的m行n列的零矩阵用粗斜体 大写字母“0”表示,特此说明.
感谢国防科技大学的刘新旺教授在百忙之中担任本书的主审,为全书的审校工作付出 了艰辛的劳动.在本书的编写过程中,刘新旺教授课题组的博士生团队参与了讨论并对书 稿提出了大量修改意见,其中,梁科、欧琦媛博士负责第1章,刘吉元、杨希洪博士负责 第2章,涂文轩、刘悦博士负责第3章,王思为、文艺博士负责第4章,梁伟轩、文艺博 士负责第5章,梁伟轩博士负责各章工作小组的联络、沟通.在此,笔者向刘新旺教授及其 团队表示衷心的感谢!
本书编辑联系邮箱为:zhangtao@ptpress.com.cn.
徐子珊
将所研究问题涉及的诸对象视为一个整体,称为集合,常用大写字母 表示. 常见的由数组成的集合有自然数集、整数集、有理数集、实数集和复数集,分别记为、、、 和 . 组成集合的每一个对象,称为该集合的一个元素,常用小写字母 表示. 若 是集合 中的元素,则记为 ,否则记为 . 例如, 但 , 但 . 若集合 中的元素均为集合 中的元素,即 ,必有 ,称 是 的子集,记为 . 例如, .
无任何元素的集合称为空集,记为 . 如果非空集合 中的元素可一一罗列出来, 则可用花括号把这些罗列出来的元素括起来. 例如,不超过 5 的正整数组成的集合 . 也可以用描述的方式表示一个具体的集合,例如,不超过 5 的正整数组成的集合可表示为 .
常将集合直观地表示为平面上的封闭区域, 集合中的元素为区域内的点, 子集的图示如图 1.1 所示.
图1.1 子集的图示
实践中所面对的问题往往涉及两个甚至更多的集合. 集合的元素之间, 通常具有某种关系.
定义1.1 设 为两个非空集合,若按确定的法则 与之对应[1], 记为 ,则称 为 到 的一个映射 (或变换),记为 . 若 ,即 ,则称 为 上的映射.
[1] 数理逻辑中,存在量词表示 “至少存在一个……”. 本书用表示 “恰存在一个……”.
若 ,称 是 的像, 是 的原像. 中元素 的像,常记为 .
例1.1 有限集合 (元素个数有限) 间的映射,可以列表表示. 设 , 定义 到 的对应法则 :
就是 中元素对应相反数的映射.
练习1.1 设 ,即比特集,其中的元素 0 和 1 是二进制数. 用列表方式表示比特集 上的映射 .
(参考答案: )
非空集合 的元素之间的对应法则 要成为 到 的映射,需满足如下两个条件.
(1) 中每个元素 均有在 中的像 .
(2) 中任一元素 在 中只有一个像 .
图 1.2(a) 表示 到 的映射; 图 1.2(b) 中由于 中存在元素在 中无像,故对应法则不是 到 的映射; 图 1.2(c) 中由于 中存在元素对应 中的两个元素,故对应法则也不是 到 的映射.
图1.2 集合元素的对应法则
例1.2 设 ,考虑函数 ,不难理解它们均为 上的映射. 然而,函数 [见图1.3(a)]是上“1-1”的映射,即一个原像 仅对应一个像 . 反之,任一 也仅有一个原像 . 这样的映射是可逆的,因为据此对应法则,我们可以得到逆映射 (即反函数) . 但是函数 [见图1.3(b)]却不是可逆的. 这是因为, 首先对于 且 ,在 中没有与之对应的原像. 其次,对于 且 中有两个原像 和 与之对应. 换句话说,根据映射 的对应法则,不能构造出其逆映射.
图1.3 可逆与不可逆函数
以上讨论的集合 到 的映射,原像均为取自集合 的一个元素,这样的映射称为一元映射. 实践中,集合 的结构也许要稍稍复杂一些.
定义1.2 设集合 和 非空,有序二元组集合 称为 与 的笛卡儿积,记为 .
在代数学中,非空集合 上的映射称为一元运算, 到 的映射称为 上的二元运算. 常用运算符来表示运算,如例 1.1 中,集合 上的取相反数的映射 即负数运算,这是一个一元运算,其运算符常表示为 “-”. ,对应 . 练习1.1 中的 上的一元映射即对比特位 (二进制位) 的取反运算,这也是一个一元运算,其运算符常表示为 “ ”. ,对应 . 利用练习1.1 的计算结果得比特集 上的取反运算表为
例1.3 考虑比特集 上的 “或” 运算 “V”: 当且仅当 时成立. 根据 运算的这一定义,可得其运算表:
练习1.2 比特集 上的 “与” 运算 “ ”: 当且仅当 时成立. 试给出 的运算表.
(参考答案:)
例1.4 自然数集 上的加法运算就是 到 的二元映射. 但是,减法运算不是 上的二元运算,因为对于 ,且 . 换句话说,对于 ,若 则 ,即 在 中没有像.
集合 上的运算结果必须属于集合 的要求,称为运算对集合 的封闭性. 不难验证,数的加法和乘法运算对 、 、 、 和 都是封闭的. 例 1.4 说明数的减法运算对 不具有封闭性,但对 、 、 和 都是封闭的. 数的除法运算对 和 不具有封闭性,但对 和 都是封闭的.
将 到 的映射 ,视为二元运算 “ ”: . 这时,需注意一个细节: 一般而言, 是一个序偶,由于 与 未必相同,故 ,但未必有 . 即使 ,但 未必与 相同. 例如, . 不全为零,则 . 对于一个二元运算 “o”: 及 , ,则称该运算具有交换律. 例如,数的加法运算 “+”,在数集 、 、 、 和 上都具有交换律.
代数学研究的主要对象是代数系统,简称代数,即非空集合 及定义在 上的若干运算 . 其中的运算 可以是一元运算,也可以是二元运算. 通常将代数记为 ,在不会产生混淆的情况下可将代数系统简记为 . 代数学对各种代数系统研究定义在 上各种运算的性质以及由这些运算及其性质所决定的集合 的逻辑结构.
例1.5 设 表示字符集, 表示由 中字符组成的有限长度的字符串全体 (含空字符串 构成的集合. ,定义 为 与 连接而成的字符串,则 构成一个代数系统, 该代数系统是信息技术中最重要的处理对象之一.
例1.6 考虑比特集 [2],练习1.1、例 1.3 及练习1.2,在 上定义的 3 个运 算分别为
[2] 中的元素 0 和 1 可视为比特位,也可视为逻辑假 (False) 和逻辑真 (True).
代数系统 即为著名的布尔代数. 其中 和 是二元运算,分别称为或和与运算. コ为一元运算, 称为非运算. 布尔代数的运算有如下性质.
(1) 或运算交换律: .
(2) 或运算结合律: .
(3) 或运算 0 -元律: .
(4) 与运算交换律: .
(5) 与运算结合律: .
(6) 与运算 1-元律: .
(7) 与运算对或运算的分配律: .
(8) 或运算对与运算的分配律: .
(9) 反演律: .
布尔代数 是数理逻辑乃至电子计算机技术的基础数学模型. 其中各运算的所有性质均可用 “真值表” 加以验证. 以证明性质 (9) 中反演律之一的 为例,说明如下:
注意,真值表中的前两列罗列出了 的所有可能的取值,而最后两列分别表示 和 在 的所有可能的取值下均相等,故 .
练习1.3 运用真值表,验证布尔代数 中的反演律 . (提示: 参考对 的证明)
我们知道,代数系统中定义在集合 上的各个运算必须是 “封闭” 的,即运算结果必须仍属于集合 .
例1.7 设 ,整数加法并非定义在 上的运算,因为虽然 ,但 . 同样地, 也不属于 . 换句话说, 中元素对整数加法运算不是封闭的. 所以 并不能够成为一个代数系统.
若将 上的 “加法” 定义为: ,
即 的运算结果是 除以 4 的余数——称为模 4 的加法. 此时 , ,因此 “+” 对 是封闭的,于是, 构成一个模 4 的剩余类加法系统 (详见例 1.10).
定义1.3 若代数 的二元运算 “ ” 具有如下性质,则称 为一个群.
(1) 结合律: .
(2) 零元律: 称为零元,
(3) 负元律: 称为 的负元,使得 . 元素 的负元常记为 .
若运算 还满足交换律,则 称为交换群.
例1.8 在比特集 上定义运算 :
称为 上的异或运算. 由上面的运算表可以看到异或运算的一个有趣之处在于: , (见运算表的第 1 行或第 1 列), (见运算表的第 2 行或第 2 列). 下面说明 构成一个交换群.
(1) 由运算表关于从左上角到右下角的对角线的对称性得知, 运算具有交换律;
(2) 构造真值表
真值表中最后两列的值完全相同,这就验证了 满足结合律 ;
(3) 由 的运算表得知 0 是零元;
(4) 由 的运算表得知 0 和 1 均为自身的负元.
交换群 在计算机的位运算中扮演着重要角色.
练习1.4 例 1.5 中的字符串代数 是否构成一个群?
(参考答案: 否. 提示: 无负元)
例1.9 代数系统 中 为全体整数的集合,+ 表示两个整数的加法运算,构成一个交换群, 称为整数群.
常将群 的运算 称为 “加法” 运算,根据 的负元律的意义及记号,可得 上的 “减法” 运算 . 因此,在例 1.9 的整数群 中既可以做加法, 也可以做减法.
例1.10 给定正整数 ,考虑集合 . 在 上定义运算
则 构成交换群.
事实上,由运算表定义的 + 运算,就是 中的两个元素之和以 为模的余数,即 为
(1) 根据运算表的对称性得知, + 运算满足交换律
(2) ,即 + 运算满足结合律
(3) 观察运算表的首行和首列可知 0 是 + 运算的零元;
(4) 且 的负元为 .
称为模 的剩余类加群.
定义1.4 若代数 的二元运算 和 具有如下性质,则称 为一个环.
(1) 对于运算 构成交换群.
(2) 运算 的结合律: .
(3) 运算 对 的分配律: 且
例1.11 整数集 对于数的加法和乘法构成的代数 构成一个环,常称为整数环. 有理数集 、实数集 以及复数集 对于数的加法和乘法构成的代数也分别构成一个环.
练习1.5 为全体自然数的集合,代数 是否构成一个环? 其中 + 和分别表示数的加法和乘法.
(参考答案: 否. 提示: 不满足负元律,不能构成一个群)
常将环 的运算 称为 “乘法”. 虽然在环 中可以进行 “加法” “减法” (“加法” 的逆运算) 和 “乘法”, 但未必能进行 “除法” —— “乘法” 的逆运算.
定义1.5 若代数 的二元运算 和 具有如下性质,则称 为一个域.
(1) 对于 和 构成一个环.
(2) 的交换律: .
(3) 的幺元律: 称为幺元,使得 .
(4) 的逆元律: 且 称为 的逆元,使得 . 非零元素 的逆元常记为 .
例1.12 在例 1.8 的交换群 的基础上添加 上的与运算 (参见例 1.6),即在 上有两个二元运算:
由例 1.8 得知, 构成一个交换群.
由例 1.6 得知, 上的与运算 满足交换律和结合律,且 1 是其幺元. 根据运算表可知, 中唯一的非零元素 1 的逆元是其本身. 最后构造真值表
根据观察,最后两列数据完全一致,可知与运算 对异或运算 具有分配律.
综上所述, 构成一个域.
例1.13 根据域的定义,不难验证代数 和 都构成域,分别称为有理数域、实数域和复数域. 但是,在整数环 中,任何非零元素对乘法运算没有逆元, 故不能构成域.
由域 中 “乘法” 的逆元律得知,对 中的任一元素 及非零元素 ,可进行“除法” 运算 . 也就是说,在域 中可以进行 “加” 减” “除” 除” “除? 四则运算. 这完全符合我们对有理数域 、实数域 及复数域 的 认知.
定义1.6 设 为一个交换群,运算 “+” 称为加法. 为一个数域,若 , ,对应唯一的元素 ,记为 ,即
常称 “.” 为数与 中元素的乘法,简称数乘法[3] . 数乘运算满足下列性质.
[3] 准确地说,数乘运算 “.” 是到的一个二元映射.
(1) 交换律: .
(2) 结合律: .
(3) 对数的加法 + 的分配律: .
(4) 对元素的加法 + 的分配律: . [4]
[4] 此处因为数域,故自身具有加法 (运算符仍用 “+”) 和乘法 (连写,不用运算符) 运算,注意在上下文中与中元素的加法和数乘运算加以区别.
中元素的加法运算 “+” 连同数乘运算 “.” 统称为线性运算. 定义了线性运算的集合 称为数域 上的一个线性代数或线性空间,记为 .
例1.14 设 为一数域, ,由符号 和常数 构成的表达式
称为数域 上 的一元多项式,简称为多项式. 其中,符号 称为变元, 称为 次项, 为 次项的系数, . 非零系数的最大下标 ,称为多项式的次数. 时,0 次多项式为一常数 . 定义常数 0 为特殊的零多项式,零多项式是唯一没有次数的多项式. 本书规定零多项式的次数为 -1 . 常用 表示多项式. 数域 上所有次数小于 的一元多项式构成的集合记为 . 两个多项式 , ,当且仅当两者同次项的系数相等,即 .
设 ,定义加法
例如, ,则 . 由于多 项式的系数来自数域 ,所以满足加法的结合律和交换律; 零多项式 为加法的零元; 对任一非零多项式 的所有系数取相反数,构成的同次多项式记为 ,为 的负元. 所以 构成一个交换群.
对任意实数 及 ,定义数乘法
例如, ,则 . 由于 和多项式的系数均 来自数域 ,故对于 与多项式系数的乘法满足交换律和结合律,且数乘法对加法满足分配律. 所以 构成一个线性空间.
例1.15 区间 上的实值可积函数全体记为 . 根据高等数学 [5] 知, . ,函数的和 ,数乘运算为 . 由于 中的任一 为实值可积函数,即函数值均为实数,而 为一个域,故有以下 结论.
[5] 见参考文献[1].
(1) 对于函数的加法,满足交换律、结合律. 零值函数 (0 为零元), ,且 ,即 有负元. 所以 构成一个交换群.
(2) 对于数与函数的乘法, ,满足 综上所述, 构成一个线性代数 (线性空间).
定义1.7 设 为定义在非空集合 上的运算, 为一代数系统. 且非空,若 也构成一个代数系统,且运算 保持在 中的所有性质,称 为 的一个子代数系统,简称为子代数.
例1.16 是 的子群, 是 的子域, 是 的子域. 但 是 的子环而不是 的子域,因为数乘法 “.” 在 中不具有在 中的逆元律. 由于集合的包含关系 具有传递性,因此子代数的关系也有传递性. 如 也是 的子域.
由定义1.7 及例 1.15 可知, 且非空,要判断 是否为代数系统 的子代数,需同时考察两个条件
(1) 运算 对子集 是封闭的;
(2) 在子集 中,运算 保持在 中的所有性质. 然而, 对线性代数 (线性空间) 而言, 有如下定理.
定理1.1 设 为数域 上的一个线性代数, 且非空, 为 的一个子线性代数 (子线性空间) 的充分必要条件是加法 “+” 和数乘法 “.”对 是封闭的.
证明 条件的必要性不证自明, 下面证明充分性. 由运算的封闭性可知, 加法 “+” 的交换律、结合律,数乘法 “.” 的结合律及对加法的分配律在 中都是保持的. 由于 是数域,因此 . 又由于数乘法 “.” 对 是封闭的,因此 ,即 含有零元. 另外, ,必有 ,有 使得 ,即 在 中有负元. 如此, 构成交换群,加之数乘法所满足的所有性质可知 是一个线性代数 (线性空间),故它为 的一个子线性代数 (子线性空间).
例1.17 由例 1.15 知,区间 上的实值可积函数全体 对函数的加法“+” 和实数与函数的数乘法 “.” 构成线性代数 (线性空间)( . 记区间 上的实值连续函数全体为 ,则 且非空[6]. 根据高等数学知识[7],实值连续函数对函数的加法和实数与函数的数乘法是封闭的. 根据定理1.1, 是 的一个子线性代数 (子线性空间).
[6] 见参考文献 [1] 第 295 页定理2 后目 1 .
[7] 见参考文献 [1] 第 119 页定理1 .
定义1.8 设两个代数系统 和 均具有 个 元运算 和 . 若存在 到 的 “ 1-1 ” 映射 ,使得对每一对运算 和 ,有
即 下 中原像的运算结果对应 中像的运算结果. 称 与 同构. 称为 与 之间的同构映射.
例1.18 考虑例 1.7 中模 4 的剩余类加群 以及代数系统 ,其中 . 两者的加法运算表为
不难验证 也是一个交换群. 建立 与 的 “ 1-1 ” 映射 为
则 的加法运算表等价于
即 下 中原像的运算结果对应 中像的运算结果. 所以, 与 同构.
以代数学的观点, 同构的代数系统被视为等同的, 只需研究其中之一, 研究结果适用于所有与之同构的代数系统.
Python 作为计算机程序设计语言, 受计算机物理结构的限制, 无法表示出完整的整数集 、有理数集 、实数集 及复数集 . 然而,Python 所模拟的 、、 和 在大多数实际应用中可以满足需求.
具体地说, Python 中的整数取值范围仅受计算机内存容量的限制, 而不受 CPU(Central Processing Unit, 中央处理器) 字长的限制. Python 的浮点数的精度和取值范围均受 CPU 字长的限制. 以 float64 为例,其有效数字位数为 15 或 16,取值范围为 . 也就是说,Python 以 16 位有效数字的有理数的集合模拟 乃至 . Python 还内置了复数类型 来模拟复数集 ,其中 表示虚数单位. 和 分别表示复数的实部和虚部, 为浮点数. Python 将整数、浮点数和复数统称为数字型数据.
表 1.1 中的运算可用于所有数字型数据上. 需要说明的是 (4) 商运算 “/” 和 (5) 整商运算 “//”, 前者运算的结果是浮点数, 而后者的运算结果是整数. Python 中, 整数不含小数, 浮点数含小数. Python 根据用户输入的数据或表达式计算结果自动判断其数据类型, 请看下面的例子.
表 1.1 数字型数据的常用运算
序号 |
运算 |
运算结果 |
备注 |
---|---|---|---|
(1) |
和 的和 |
||
(2) |
减 的差 |
||
(3) |
x * |
和 的乘积 |
|
(4) |
除 的商 |
||
(5) |
除 商的整数部分 |
||
(6) |
x % y |
的余数 |
x- |
(7) |
x ** |
的 次幂 |
|
(8) |
取反 |
单目运算 |
|
(9) |
不变 |
单目运算 |
例1.19 Python 中数字型数据的算术运算.
程序1.1 Python 的数字型数据
1 a=4 #整数
2 b=3 #整数
3 c=a+b #整数
4 print(c,type(c))
5 c=a/b #浮点数
6 print(c,type(c))
7 c=a//b #整数
8 print(c,type(c))
9 b=3.0 #浮点数
10 c=a+b #浮点数
11 print(c,type(c))
12 a=0.1+0.2 #浮点数@(0.1+0.2)@
13 b=0.3 #浮点数@(0.3)@
14 print(a= =b) #浮点数相等判断
15 print(abs(a-b)<1e-10) #浮点数比较
16 a=1+2j #复数
17 c=a/b #复数
18 print(c,type(c))
程序的第 1、 2 行输入整数 4 和 3 (没有小数) 赋予变量 a 和 b. 注意, Python 用 “=”作为为变量赋值的运算符,判断两个数据是否相等的比较运算符为 “ ”. 第 3 行将运算得到的 a 与 的和 赋予变量 ,由于加法运算对整数是封闭的 (运算结果仍为整数),故 亦为整数. 第 4 行输出 c.
第 5 行将 与 的商 赋予 ,由于整数集 构成环 (参见例 1.11),而不构成域 (参见例 1.13),故对于除法运算不是封闭的. 此时, 自动转换为浮点数. 第 6 行输出 .
第 7 行将 a 与 的整数商 (即)赋予 . 此时,运算结果为整数. 故 为整数. 第 8 行输出 c.
在表达式中既有整数又有浮点数混合运算时, Python 会将表达式中的整数计算成分自动转换成浮点数, 运算的结果自然为浮点数. 第 9 行将浮点数 3.0 (带小数) 赋予 b, 第 10 行将 与 的和 赋予 . 注意此时 是整数 (4), 是浮点数 (3.0),故 是浮点数. 第 11 行输出 c.
第 12 行将浮点数 0.1 与 0.2 的和赋予 ,第 13 行将浮点数 0.3 赋予 b. 第 14 行输出相等比较表达式 的值: 若 的值与 的值相等,该值为 “True”,否则为 “False”. 第 15 行输出小于比较表达式 ,即 的值: 若 与 的差的绝对值小于 ,该值为 “True”,否则为 “False”. 按常识,这两个判断输出的值应该都是 "True". 但是, 和 存储的是浮点数,它们只有有限个有效位,在有效位范围之外的情形是无法预测的. a 作为 0.2 (带有无效位) 与 0.3 (带有无效位) 的和, 将两个浮点数所带的无效位通过相加传递给运算结果, 产生了和的无效位, 这可能会产生 “放大” 无效位效应. 事实上, 将其与存储在 中的浮点数 0.3 进行相等比较 (第 14 行),得出的结果是 “False”. 为正确地比较两个浮点数 和 是否相等,采用第 15 行的办法: 计算两者差的绝对值 ,考察其值 是否 “很小”——小于一个设定的 “阈值”,此处为 即 ,此时结果为 “True”. 第 16 行将复数 赋予变量 ,虽然输入的实部 1 和虚部 2 均未带小数,但 Python 会自动将其转换成浮点数. 第 17 行将算得的 赋予 ,第 18 行输出 .
运行程序, 输出
7 <class `int'>
1.3333333333333333 <class `float'>
1 <class `int'>
7.0 <class `float'>
False
True
(0.3333333333333333+0.6666666666666666j) <class `complex'>
注意, 本例输出每一个数据项, 同时显示该数据项的类型.
所有的有理数都可以表示成分数. Python 有一个附加的分数类 Fraction, 用于精确表示有理数.
例1.20 有理数的精确表示.
程序1.2 Python 的分数
1 from fractions import Fraction as F #导入Fraction
2 a = F(4,1) #4的分数形式
3 b = F(3) #~3的分数形式
4 c = a/b #分数4/3
5 print(c)
6 d = F(1/3) #用浮点数初始化分数对象
7 print(d) #输出分数近似值
8 print(d.limit_denominator()) #最接近真值的分数
分数用 Fraction 的初始化函数创建, 其调用格式为
Fraction(d, v).
参数 和 分别表示分数的分子和分母. 整数的分母为 1,可以省略. 程序中的第 1 行导入 Fraction,为简化代码书写,为其取别名为 . 第 行分别将 4 和 3 以分数形式赋予变量 和 . 前者以分子、分母方式输入,后者省略分母 1 . 第 4 行计算 与 的商 并将其赋予 c. 第 5 行输出 c. 第 6 行用浮点数 初始化分数对象并将其赋予有理数 第 7 行输出 ,它是无穷小数 的近似值. 第 8 行调用 Fraction 对象的成员方法 limit_denominator,算得最接近 的分数,即 . 运行程序,将输出
4/3
6004799503160661/18014398509481984
1/3
读者可将此与程序1.1 中输出的相应数据项进行对比
Python 中所有的关系运算结果均为布尔值: 非 True 即 False. 其常用关系运算符罗列在表 1.2 中.
表 1.2 常用关系运算符
序号 |
运算符 |
含义 |
---|---|---|
(1) |
严格小于 |
|
(2) |
小于或等于 |
|
(3) |
严格大于 |
|
(4) |
大于或等于 |
|
(5) |
等于 |
|
(6) |
不等于 |
|
(7) |
in |
元素在集合内 |
Python 可实现例 1.6 中讨论的布尔代数 . 其中, True,False . 分别用运算符 "or""and" 和 "not" 表示 V、 和 . 关系运算和布尔代数是程序设计中循环和分支语句的 “灵魂”, 在下面的例子中可见一斑.
例1.21 例 1.14 中讨论了数域 上的多项式集合 . 我们知道,系数序列 , 确定了 . 希望用 Python 根据存储在数组 a 中的系数序列, 输出表示对应的多项式表达式的字符串:
其中, 表示多项式的 次项系数 在数组 a 中的第 个元素值. 约定: 零多项式的系数序列为空“[ ]”.
解 解决本问题需考虑如下几个关键点:
(1) 零多项式需特殊处理;
(2) 常数项,也就是 0 次项不带字符 的幂;
(3) 1 次项的字符 不带幂指数,即输出 ;
(4) 负系数自带与前项的连接符 “-”, 非负系数需在前面加入连接符 “+”;
(5) 从 2 次项起,各项输出的规律相同,即 .
Python 中的 list 对象和 NumPy 的 array 对象均可作为存储序列的数组, 解决本问题的 Python 代码如下.
pdf 22
程序1.3 构造多项式表达式
1 import numpy as np #导入NumPy
2 from fractions import Fraction as F #导入Fraction
3 def exp(a): #多项式表达式
4 n=len(a) #系数序列长度
5 s=`' #初始化空字符串
6 if n==0: #零多项式
7 s=s+`0'
8 else: #非零多项式
9 for i in range(n): #对每一项
10 if i==0: #常数项
11 s=s+`%s'%a[i]
12 if i==1 and a[i]>=0: #非负1次项
13 s=s+`+%s·x'%a[i]
14 if i==1 and a[i]<0: #负1次项
15 s=s+`%s·x'%a[i]
16 if i>1 and a[i]>=0: #非负项
17 s=s+`+%s·x**%d'%(a[i],i)
18 if i>1 and a[i]<0: #负项
19 s=s+`%s·x**%d'%(a[i],i)
20 return s #返回表达式
21 a=[1,-2,1]
22 b=[F(0),F(-1,2),F(0),F(1,3)]
23 c=np.array([0.0,-0.5,0.0,1/3])
24 d=[]
25 print(exp(a))
26 print(exp(b))
27 print(exp(c))
28 print(exp(d))
本程序中将完成功能的代码组织成一个自定义函数——个可以按名调用的模块. Python 的函数定义语法是
def 函数名 (形式参数表):
函数定义体
本程序中第 行定义的函数名为 ,形式参数表中仅含的一个参数表示存储多项式系数序列的数组 a. 函数定义体内罗列出函数处理数据的操作步骤. exp 函数中, 第 4 行调用 Python 的 len 函数计算数组 a 的长度,即所含元素个数,并将其赋予变量 . Python 中的字符串类型实现了例 1.5 中讨论的代数系统 ,其中 为 ASCII 符号集,+ 运算符用于连接两个字符串. Python 的字符串常量是用单引号括起来的字符序列. 第 5 行将表达式串初始化为空字符串. 第 行的 if-else 分支语句根据 a 的长度是否为 0 分别处理零多项式和非零多项式. 对于零多项式,第 7 行直接将单字符串 (0 添加到空字符串 之后. 处理非零多项式的第 行的 for 循环语句,扫描数组 a,处理多项式的每一项. 第 10、 11 行的 if 语句处理常数项,注意第 11 行中连接到 尾部的 ‘’ , 称为格式串,串中 “%s’ 称为格式符, 意为以串中指定的格式加载单引号后面的数据项 a[i]. 格式串的一般形式为
含格式符的串’%(数据项表).
含格式符的串中格式符的个数与数据项表中数据项的个数必须相同, 若数据项表中仅有一个数据项,括号可省略,如第 11 行中的格式串. 常用格式符包含表示字符串的格式符 ,表示十进制整数的格式符 ,表示十进制浮点数的格式符 ,等等. 类似地,第 12、 13 行处理非负 1 次项; 第 14、 15 行处理负 1 次项; 第 16、17 行处理以后的非负项; 第 18、 19 行处理负项. 循环结束,第 20 行返回字符串 .
程序的第 21 行用 list 对象 a 表示多项式 的系数序列 ,其中的元 素为整数; 第 22 行用 list 对象 表示多项式 的系数序列,元素类型为 Fraction; 第 23 行用 NumPy 的 array 对象 的数组 表示多项式 的系数序列,注意 的 array 对象可以用 list 对象 初始化; 第 24 行用空的 list 对象 表示零多项式系数序列. 第 行分别调用 函数和 print 函数输出 a、b、c、d 的表达式. 运行程序,输出
1-2·x+1·x**2
0-1/2·x+0·x**2+1/3·x**3
0.0-0.5·x+0.0·x**2+0.3333333333333333·x**3
0
练习1.6 程序1.3 定义的构造多项式表达式的函数 exp 并不理想: 它将系数为 0 的项也表示在表达式中, 显得有点笨拙. 修改 exp, 在所创建的表达式中忽略系数为 0 的项. (参考答案: 见文件 chapter01.ipynb 中相应的代码)
Python 中整数在计算机内部是按二进制格式存储的, 每一位非 0 即 1 . 因此, 每一位都构成了例 1.6 中的布尔代数 以及例 1.12 中的域 . 两个整数 与 的位运算指的是: 若 与 的二进制位数相同则对应位进行相应的运算,否则对齐最低位,位数少的高位补 0 , 然后对应位进行相应运算. Python 的位运算如表 1.3 所示.
表 1.3 Python 的位运算
序号 |
运算 |
运算结果 |
备注 |
---|---|---|---|
(1) |
和 按位或 |
||
(2) |
x 和 v 按位异或 |
||
(3) |
x&y |
和 按位与 |
|
(4) |
对 带符号位的补码逐位 |
||
(5) |
左移 位 |
必须是非负整数 |
|
(6) |
右移 位 |
必须是非负整数 |
需要说明的是:
(1) 并非对整数 的二进制原码逐位取反,要实现整数原码逐位取反只需对每一位与 1 做异或运算即可 (参见例 1.8);
(2) 左移运算表示将整数 向左每移动一个二进制位,右端添加一位 0,即 相当于将 乘 . 相仿地, 相当于将 除以 .
int 对象的函数 bit_length 用于计算并返回整数的二进制表达式的长度 (位数), 例如,设 a 中整数为 286,则 a.bit_length() 将返回 9 . 注意, 的二进制表达式为 . Python 的 bin 函数用于将整数转换成二进制表达式,例如, 将返回字 符串 ’0b1111111’.
例1.22 下列代码说明这些运算符的运用.
程序1.4 Python 的位运算
1 a=17
2 b=21
3 print(`a=%s'%bin(a)) #a的二进制表达式
4 print(`b=%s'%bin(b)) #b的二进制表达式
5 print(`a<<2=%s, %d'%(bin(a<<2),a<<2)) #a左移@2@位
6 print(`b>>2=%s, %d'%(bin(b>>2),b>>2)) #b右移@2@位
7 print(`a|b=%s'%bin(a|b)) #a与@b@按位或
8 print(`a&b=%s'%bin(a&b)) #a与@b@按位与
9 print(`a^b=%s'%bin(a^b)) #a与@b@按位异或
10 print(`~a=%s'%bin(~a)) #a的带符号位补码逐位取反后的补码
11 n=a.bit_length() #a的二进制表达式的位数
12 b=2**n-1
13 print(`a逐位取反=%s'%bin(a^b)) #a的原码逐位取反
程序的第 1、2 行设置两个整数变量,分别初始化为 17 和 21 . 第 3、 4 行分别调用 bin 函数以二进制格式输出 和 . 第 5 行输出 左移 2 位后的二进制表达式和十进制表达式.第 6 行输出 右移 2 位后的二进制表达式和十进制表达式. 第 7 行输出 a 与 的按位或的计算结果. 第 8 行输出 与 的按位与的计算结果. 第 9 行输出 与 的按位异或的计算结果. 第 10 行对 a 的带符号位的补码逐位取反: a 的原码 10001 的最高位之前还有一个符号位,由于 是整数,故符号位为 0,即
正整数的反码和补码就是原码, 逐位取反后得
为一负数 (符号位为 1),其补码是其反码 加 1 的结果,即
第 行计算并输出 a 的原码逐位取反的结果: 第 11 行调用整数对象 a 的 bit_length 函数计算 a 的二进制位数并将其赋值给 ,第 12 行利用 算得 位均为 1 的二进制整数 并将其赋值给 b,第 13 行利用 a 与 的按位异或得到对 的原码逐位取反的结果并将其输出. 运行程序, 输出
a=0b10001
b=0b10101
a<<2=0b1000100, 68
b>>2=0b101, 5
a|b=0b10101
a&b=0b10001
a^b=0b100
~a=-0b10010
a逐位取反=0b1110
定理1.2 给定正整数 ,记 ,即 是所有 位二进制非负整数构成的集合. 代数系统 构成一个环. 其中, 表示 中整数的按位异或, 表示 中整数的按位与.
证明 首先,由整数按位运算的意义可知, . 其次根据例 中元素对按位异或 满足交换律、结合律. 0 为零元, 中任一元素 为自身的负元. 按定义1.3 知, 构成一个交换群. 根据例 1.6、例 1.8 及例 1.12 知 运算具有交换律、结合律以及 对 具有分配律. 为关于 的幺元,因为 . 按定义1.4, 构成一个环.
结合例 1.12 及定理1.2 得知, 时 构成一个域, 时 构成一个环.
例1.23 TCP/IP(Transmission Control Protocol/Internet Protocol, 传输控制协议/互联网协议) 在互联网中的应用是, 每一个节点都有一个 IP 地址. 对 IPv4 而言, 一个 IP 地址 就是一个 32 位二进制非负整数,即 . 为方便记,通常将此整数的二进制表达式分成 4 节, 每节 8 位 (1 字节), 用点号 “. ” 隔开. 例如, 一个节点的 IP 地址为整数 3232236047, 其二进制表达式为
11000000.10101000.00000010.00001111.
第 1 节的 11000000 对应十进制整数 192, 第 2 节的 10101000 对应 168, 第 3 节的 00000010对应 2 , 第 4 节的 00001111 对应 15 . 在文献中为简短计, 常将其记为
TCP/IP 规定每个 IP 地址分成网络号和主机号两部分,并将所有 个 IP 地址分成 A、B、C、D、E 共 5 类. 常用的 A、B、C 这 3 类地址的定义如表 1.4 所示
表 1.4 3 类地址的定义
类别 |
网络号 |
主机号 |
---|---|---|
A |
第 1 字节,取值范围为 |
后 3 字节 |
B |
前 2 字节,第 1 字节取值范围为 |
后 2 字节 |
C |
前 3 字节,第 1 字节取值范围为 |
最后一字节 |
路由程序用 “掩码” 来分析 IPv4 地址 的类型、网络号和主机号. 具体介绍如下:
(1) 掩码 255.0.0 与 按位与,然后将结果右移 24 位得到地址的第一字节取值,以此判断网络类型;
(2) 根据 (1) 得到的网络类型,设置 型网络掩码 为 255.0.0.0,B 型网络掩码 为 255.255.0.0,C 型网络掩码 为 255.255.255.0;
(3) 与掩码 的按位与并将结果右移若干位 (A 类地址右移 24 位, 类地址右移 16 位, 类地址右移 位) 得网络号;
(4) 对由 (3) 算得的结果逐位取反得 ,计算 与 的按位与结果得到主机号.
下列程序对给定的表示 IPv4 地址的整数 判断其网络类型并分析其网络号及主机号.
程序1.5 IPv4 地址分析
1 def ipAnaly(a):
2 b32=2**32-1 #32位1
3 m=255<<24 #掩码255.0.0.0
4 t=(a&m)>>24 #地址的第1字节
5 if 1<=t<=126: #A类地址
6 t1=`A' #地址类型
7 n=t #网络号
8 m1=m^b32 #掩码255.0.0.0的反码
9 p=a&m1 #主机号
10 if 128<=t<=191: #B类地址
11 t1=`B' #地址类型
12 m=(2**16-1)<<16 #掩码255.255.0.0
13 n=(a&m)>>16 #网络号
14 m1=m^b32 #掩码的反码
15 p=a&m1 #主机号
16 if 192<=t<=223: #C类地址
17 t1=`C' #地址类型
18 m=(2**24-1)<<8 #掩码255.255.255.0
19 n=(a&m)>>8 #网络号
20 m1=m^b32 #掩码的反码
21 p=a&m1 #主机号
22 return t1, n, p
23 a=2005012608 #地址119.130.16.128
24 print(ipAnaly(a))
25 a=2282885253 #地址136.18.16.133
26 print(ipAnaly(a))
27 a=3321996052 #地址198.1.163.20
28 print(ipAnaly(a))
程序的第 行定义用于 IP 地址分析的函数 ipAnaly,该函数仅有一个表示待分析的 IP 地址的整数参数 a. 函数体中,第 2 行用 设置用于原码求反的有 32 位 1 的整数变量 b32; 第 3 行用 将掩码 设置为第 1 字节全为 1,其他全为 0,即 255.0.0 .0, 用于分析地址 a 的第 1 字节以判断地址类型; 第 4 行用按位与 a&m 算出 a 中第 1 字节后面 3 字节全为 0,然后 右移 24 位得 a 的第 1 字节值,将其赋予 ; 第 5 个 行、第 行、第 行的 if 语句分别就判断算得的 而得到的 3 种地址类型分析计算网络号 和主机号 . 以第 行的 if 语句为例加以说明,对于另外两个情形,读者可参考代码的解释信息阅读理解. 第 10 行测得地址类型为 B, 第 11 行将字符 ’B’ 赋予 t1; 第 12 行将掩码 设为前两字节为 1,其余全为 0,即 255.255.0.0 ; 第 13 行按位与 a& 计算 a 的前两字节及后两字节为 0 的字节构成的整数, 则将该整数右移 16 位,得到 a 的前两字节的整数值,将其赋予网络号 ; 第 14 行按位异或 计算 的逐位取反结果,即前两字节为 0 后两字节为 1,将其赋予 ; 第 15 行按位与 算得 a 的后两字节的值, 将其赋予主机号 p.
第 23、24、25、26 和 27、28 行分别对表示 IP 地址的整数 2005012608(119.130.16.128)、 2282885253(136.18.16.133) 和 3321996052(198.1.163.20) 调用函数 ipAnaly 分析地址的类型、网络号及主机号并将其输出. 运行程序, 输出
(’A’, 119 , 8523904 )
(’B’, 34834, 4229)
(’C’, 12976547, 20)
Python 是一门面向对象的程序设计语言, 可以用类的定义方式来自定义代数系统: 定义类中对象 (集合元素) 所具有的属性以及对象间的运算. Python 为顶层抽象类保留了对应各种运算符的虚函数, 我们只需在类的定义中重载所需的运算符虚函数就可使用它们.
例1.24 考虑用 Python 实现例 1.14 中定义的多项式线性空间 .
首先, 定义多项式类 myPoly.
程序1.6 多项式类的定义
1 import numpy as np #导入NumPy
2 class myPoly: #多项式类
3 def __init__(self, coef): #初始化函数
4 c=np.trim_zeros(coef,trim=`b') #删除高次零系数
5 self.coef=np.array(c) #设置多项式系数
6 self.degree=(self.coef).size-1 #设置多项式次数
7 def __eq__(self, other): #相等关系运算符函数
8 if self.degree!=other.degree: #次数不等
9 return False
10 return (abs(self.coef-other.coef)<1e-10).all()
11 def __str__(self): #生成表达式以供输出
12 return exp(self.coef)
Python 自定义类的语法格式为
class 类名:
类定义体
类定义体内定义所属的各函数. 程序1.6 中,第 行所定义的多项式类名为 “myPoly”. 类定义体中罗列了 3 个函数: 第 行重载的初始化函数 init、第 行重载的相等关系运算符函数 和第 行定义的用于输出表达式的函数 .
Python 的类中的函数分成类函数和对象函数两种. 类函数从属于类, 其调用格式为
类名. 函数名 (实际参数表).
对象函数从属于类的对象, 其调用格式为
对象. 函数名 (实际参数表).
myPoly 类中罗列的 3 个函数都是对象函数, 其定义特征为函数的形式参数表中均含表示对象的 self. 换句话说, 类函数的形式参数表中不含参数 self.
Pyhon 的顶层抽象类中已经定义了部分虚函数, 如几乎每个类都必需的初始化函数 init, 以及各种常用的运算符函数. 重载时这些函数的特征是函数名的首、尾各有两个下画线, 如程序1.6 中重载的 __init__ 、 __eq__ 和 __str__ 函数, 程序员要做的是按需实现这些函数. 普通函数的定义中函数名前后不用如此修饰.
根据例 1.14 中多项式的定义可知 由其次数 及 个系数构成的序列
所确定,变量是取符号 还是取别的符号无关紧要. Python 中表示序列的数据类型有 list, 还可以使用 NumPy 包中的数组 array 类. 无论是 list 还是 array 的对象, 它们的下标与我们所定义的多项式的系数序列的元素下标一致, 也是从 0 开始的. NumPy 是快速处理数组的工具包, 要使用其中的工具模块需事先导入它, 这就是程序1.6 的第 1 行的任务.
第 行重载的 init 函数中,除了表示创建的多项式对象参数 self,还有一个表示多项式系数序列的数组参数 coef, 该参数既可以是 Python 的 list 对象也可以是 NumPy 的 array 对象. 第 4 行调用 NumPy 的 trim_zeros 函数, 消除参数 coef 的尾部可能包含的若干个 0 . 注意传递给命名参数 trim 的值为 ’ ’,表示操作是针对序列 coef 的尾部的. 第 5 行用参数 coef 的数据创建对象自身的 array 型属性 coef. 第 6 行用系数序列的长度 -1 初始化对象的次数属性 degree. 需要注意的是, 当参数 coef 传递进来的是元素均为 0 的数组, 即系数均为 0 的零多项式时,第 4 行操作的结果 成为一个空 (没有元素) 的数组,第 6 行的操作使得多项式对象的次数 degree 为 -1 . 这与我们在例 1.14 中的约定保持一致.
第 行重载的是表示两个多项式是否相等的关系运算符 “ ” 的 eq 函数. 该函数有两个参数: 表示多项式对象自身的 self 和另一个多项式对象 other. 其返回值是一个布尔型数据: True 或 False,表示 self 和 other 是否相等. 第 8、 9 行的 if 语句检验两个多项式的次数是否不等, 若不等则返回 False. 第 10 行针对两个次数相同的多项式判断它们是否相等. 我们知道 self 和 other 均具有 NumPy 的 array 对象 coef, 表达式 self.coef-other.coef 表示两个等长数组按对应元素相减得到数组,即 . abs(self.coef-other.coef) 则表示数组 ,而 abs(self.coef-other.coef) <1e-10 则表示数组
其中的每一项都是布尔型数据: 非 True 即 False. (abs(self.coef-other.coef)<1e-10).all() 则表示上述数组中的所有项是否都是 True. 这正是判断两个等长的浮点型数组的对应元素是 否相等,若返回 True,则意味着以 的精确度,断定两个等次多项式相等,否则认为两个 多项式不等.
第 11、 12 行重载的对象函数 str 的功能是利用对象自身的 coef 数组数据生成多项式的表达式以供输出时使用: 调用 print 函数输出 myPoly 对象时后台自动调用此函数. 该函数只有一个表示多项式对象自身的参数 self, 在函数体中简单调用我们在程序1.3 中定义的 exp[8] 函数即可. 用下列代码测试 类.
[8] exp已按练习1.6的要求修改.
程序1.7 测试多项式类
1 import numpy as np #导入NumPy
2 from fractions import Fraction as F #导入Fraction
3 p=myPoly(np.array([1,-2,1])) #用NumPy的array对象创建多项式p
4 q=myPoly([F(0),F(-1,2),F(0),F(1,3)])#用Python的list对象创建多项式q
5 r=myPoly([0.0,-0.5,0.0,1/3]) #用list对象创建多项式r
6 print(p) #输出p的表达式
7 print(q) #输出q的表达式
8 print(r) #输出r的表达式
9 print(p= =q) #检测p与q是否相等
10 print(q= =r) #检测q与r是否相等
程序的第 3 行用整数构成的 array 对象 作为系数序列创建 对象 p. 注意, 形式上似乎在调用一个与 myPoly 类同名的函数, 实际上在调用 myPoly 的 init 函数创建多项式对象. 第 4 行用 list 对象 创建分数型系数的多项式 q. 第 5 行用 list 对象 创建浮点型系数的多项式 . 第 行分别输出各自的表达式,检测重载的 init 函数和 str 函数. 第 9、 10 两行分别检测 与 是否相等、 与 是否相等、即检测重载的 eq 函数. 运行程序,输出
1-2·x+1·x**2
-1/2·x+1/3·x**3
-0.5·x+0.3333333333333333·x**3
False
True
接下来, 我们在 myPoly 类中重载多项式的线性运算: 加法运算和数乘运算.
程序1.8 多项式类的线性运算
1 import numpy as np #导入NumPy
2 class myPoly: #多项式类
3 ...
4 def __add__(self, other): #运算符“+”
5 n=max(self.degree,other.degree)+1 #系数个数
6 a=np.append(self.coef,[0]*(n-self.coef.size)) #补长
7 b=np.append(other.coef,[0]*(n-other.coef.size)) #补长
8 return myPoly(a+b) #创建并返回多项式和
9 def __rmul__(self, k): #右乘数k
10 c=self.coef*k #各系数与k的积
11 return myPoly(c)
程序第 3 行中的省略号表示程序1.6 中已定义的对象函数. 第 行按例 1.14 定义的多项式加法定义重载运算符 “+” 的函数 add. 该函数的两个参数: self 表示多项式对象本身, other 表示参加运算的另一个多项式对象. 第 5 行调用系统函数 max 计算 self 及 other 中次数的最大者并 +1,将结果赋予 . 第 两行调用 NumPy 的 append 函数利用 将两个多项式的系数序列 和 调整为相同长度. 注意,append 函数的作用是将传递给它的两个参数首尾相接. 第 8 行用 a 与 按元素求和得到的序列创建 myPoly 对象并将其返回.
我们已经看到重载的相等运算符 “ ” 函数实际上是函数 ,它是自身对象 self 的函数. 表达式 的作用实际上是调用 的 函数, 扮演了第一个参数 是传递给 的 other 参数. 此处重载的加法运算符 “+” 函数的参数意义也是如此. 然而,数乘运算就不能简单地重载乘法运算符 “*” 函数 mul, 因为参加运算的一个是多项式 (参数 self), 另一个是数 k. 换句话说,self 参数表示的是多项式 ,调用时就应写成 ,这不符合大多数人的习惯. 因此,程序1.8 的第 行重载的是 “右乘” 运算符 “*” 函数 rmul,调用时多项式可写在运算符的右边: . 该函数的第一个参数 self 表示多项式对象,它作为右运算数,第二个参数表示左运算数 . 第 10 行用 按元素乘 self 的系数序列 coef,将结果赋予 c. 第 11 行用 创建结果多项式并将其返回.
例1.25 下列代码用于测试多项式的线性运算.
程序1.9 测试多项式的线性运算
1 import numpy as np #导入@NumPy@
2 from fractions import Fraction as F #导入@Fraction@
3 p=myPoly(np.array([1,-2,1])) #用@NumPy@的@array@对象创建多项式@p@
4 q=myPoly([F(0),F(-1,2),F(0),F(1,3)])#用@Python@的@list@对象创建多项式@q@
5 k=1.5
6 print(p+q)
7 print(k*q)
运行程序, 输出
1-5/2·x+1·x**2+1/3·x**3
-0.75·x+0.5·x**3
将程序1.6、程序1.8 定义的 myPoly 类代码写入文件 utility.py, 以便调用.
练习1.7 利用 myPoly 类中定义的加法运算符函数和数乘运算符函数重载减法运算符函数 __sub__ , 并加以测试.
(参考答案: 参见文件 chapter01.ipynb 中相应代码)