书名:线性代数与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 中相应代码)