第3 章数值计算 3.1 数值数据类型 计算机刚研发出来时,它们主要被视为数字处理器,也就是协助科学家完成科学计算 的工作,现在这仍然是一个重要的应用。正如你所见,涉及数学公式的问题很容易转化为 Python程序。在本章中,我们将仔细考查一些程序,这些程序的目的是执行数值计算。 计算机程序存储和操作的信息通常称为“数据”。不同类型的数据以不同的方式存储和操 作。比如下面这个计算零钱的程序: #3_1 change.py #A program to calculate the value of some change in dollars def main(): print("Change Counter") print() print("请输入你的各种硬币个数.") yuan = eval(input("有多少1 元的硬币: ")) fifty_cents = eval(input("有多少5 角的硬币: ")) twenty_cents = eval(input("有多少2 角的硬币: ")) ten_cents = eval(input("有多少1 角的硬币: ")) total =yuan * 1.0 + fifty_cents * .50 + twenty_cents * .20 + ten_cents * .10 print() print("你拥有的硬币总额是", total) main() 下面是输出示例: Change Counter 请输入你的各种硬币个数. 有多少1 元的硬币: 5 有多少5 角的硬币: 3 有多少2 角的硬币: 4 第3 章 数值计算 有多少1 角的硬币: 6 你拥有的硬币总额是7.9 这个程序实际上操作两种不同的数字。用户输入的值(5,3,4,6)是整数,它们没有任 何小数部分。硬币的值(1.0,.50,.20,.10)是分数的十进制表示。在计算机内部,整数和具 有小数部分的数值的存储方式是不同的,从技术上讲,这是两种不同的“数据类型”。 对象的数据类型决定了它可以具有的值以及可以对它执行的操作。整数用int表 示,int型的值可以是正数和负数。具有小数部分的数字为浮点(float)型。当需要判断数 字是什么数据类型时,可以使用Python内置的type()函数,如下所示。 >>> type(5) <class 'int'> >>> type(5.0) <class 'float'> >>> a = 5 >>> type(a) <class 'int'> >>> a = 5.0 >>> type(a) <class 'float'> 从上面可以看到两种数据类型:int型和float型。那为什么会有多种数据类型呢? 其 中的一个原因是程序风格,如int型表示的数据不能带有小数部分,比如不能说有3.8只小 动物等;另一个原因是各种数据类型的操作效率问题,如对int型的处理,计算机一般比较 快。当然,当今的处理器,CPU的浮点运算是高度优化的,可能与int型运算一样快。 int型和float型之间的另一个区别是,float型可以表示数学中的实数。我们会看 到,存储值的精度(或准确度)存在限制。由于float型不精确,而int型总是精确的,所以 一般的经验法则应该是:如果不需要小数值,就使用int型。 当然,Python除了支持int型和float型,还支持长整(long)型和复数(complex)型。 表3-1列出了一些数值类型的示例。 表3-1 一些数值类型的示例 int long float complex 10 519243964254752 0.0 3.14j -10 -47218852996548 3.21 3j 0o56(八进制) 0o5734542234233251(八进制) 56.25 9.322e-32j -0o56(八进制) -0o573454223433251(八进制) -32.54e100 3e+23j 0x10(十六进制) 0xDEFABCECBDAECBFBAE -0x10(十六进制) -0x19323 35 Python 程序设计基础 >>> i = 10 >>> i 10 >>> a = 0o56 >>> a 46 >>> b = 0x10 >>> b 16 >>> c = 519243964254752 #十进制长整型 >>> c 519243964254752 >>> d = 0o56345422342345223251 #八进制长整型 >>> d 836738412174452393 >>> e = 0xDEFABCECBDAECBFBAE #十六进制长整型 >>> e 4113244760468049623982 >>> f = -32.54e100 #浮点数 >>> f -3.254e+101 >>> g = 3e+23j #复数3e 为实部,23j 为虚部 >>> g 3e+23j 值的数据类型决定了可以进行什么样的操作。Python支持对数值的一般数学运算。 表3-2总结了这些操作。 表3-2 Python内置的数值操作 操 作 符操 作 + 加 - 减 * 乘 / 浮点除 操 作 符操 作 ** 指数 abs() 绝对值 // 整数除 % 取余 看看如下的Python交互: >>> 3 + 4 7> >> 3.0 + 4.0 7.0 >>> 3 * 4 36 第3 章 数值计算 12 >>> 3.0 * 4.0 12.0 >>> 4**3 64 >>> 4.0**3 64.0 >>> 4.0**3.0 64.0 >>> abs(5) 5> >> abs(-3.5) 3.5 在大多数情况下,程序员不必明确操作的是什么类型的数据。整数加法和浮点数加 法计算方法相同。但除法会有所不同,在Python3.0之后提供了两种不同的运算符,以 前常见的符号斜线(/)用于常规除法,双斜线(//)用于整型除法。通过代码来比较它们的 差异: >>> 100 / 3 33.333333333333336 >>> 100 // 3 33 >>> 100.0 / 3.0 33.333333333333336 >>> 100.0 // 3.0 33.0 >>> 100.00 // 3.00 33.0 >>> 100 % 3 1> >> 100.0 % 3.0 1.0 请注意,操作符“/”总是返回一个浮点数。即使操作数是int型,除法产生的结果也 是带小数的浮点数。这与C 系列语言和Java语言有所不同。你可能注意到了,在 Python中,100/3和100.0/3.0的结果最后都有一个6,这是因为浮点数总是近似值。 要获得返回整数结果的除法,可以使用整数除法运算“//”,因为整数除法总是产生一 个整数。可以把整数除法看作整除。表达式10//3得到3。虽然整数除法的结果总是一 个整数,但结果的数据类型却取决于操作数的数据类型。整数整除浮点数得到一个浮点 数,它的分数分量为0。 最后两行展示了余数运算%。请再次注意,结果的数据类型取决于操作数的类型。 37 Python 程序设计基础 求余数的操作采用“%”,可以把一个数用“//”和“%”表示出来,例如: a=(a//b)(b)+(a%b) 3.2 类型转换和舍入 前面已经知道了同种数据类型数据的基本运算,但在某些情况下,可能会将两种不同 的数据类型进行计算,这时就要将一种数据类型转换成另一种数据类型进行运算。例如 一个整型数据与一个浮点型数据相加,此时Python会如何处理? 比如计算下面示例: x = 5.0 + 2 如果两者都为整型或都为浮点型,此时结果很简单,分别为7和7.0。当遇到示例这 种情况时,我们首先想到的是将两者变为同一种数据类型,其中一种是将浮点型5.0变为 整型5,然后再与2相加;另一种是将整型2变为浮点型2.0,再与5.0相加。 通常情况下,将浮点型转换为整型会使计算结果不精确,从而导致发生严重的错误, 因为当浮点型小数点后不为0时,将浮点型转换为整型,小数部分就会被截去,从而导致 结果错误。而将整型转换为浮点型,只需要在整型后加上小数点即可,所以,在混合类型 表达式中,Python会自动将整型转换为浮点数,并执行浮点运算以产生浮点数结果,这种 转换,称为“隐式转换”。 当然,程序员也可以通过显式转换来指定数据的类型转换。Python提供了内置的转 换函数int()和float()。 >>> int(3.14) 3> >> float(3) 3.0 >>> float(int(3.14)) 3.0 >>> int(float(3)) 3 如你所见,转换为整型就是丢弃浮点值的小数部分,该值将被截断,而不是舍入。对 数值进行四舍五入的通常方法是使用内置的round()函数,它可以将数值四舍五入到最 接近的整数值。 >>> round(3.14) 3>>> round(3.5) 4 38 第3 章 数值计算 请注意,像这样调用round()函数将会产生一个整型的值。因此,对round()的简单 调用是将浮点型转换为整型的另一种方法。 >>> pi = 3.141592653589793 >>> round(pi, 2) 3.14 >>> round(pi,3) 3.142 类型转换函数int()和float()还可以将数字字符串转换为数字类型: >>> int("1234") 1234 >>> float("1234") 1234.0 >>> float("1234.5") 1234.5 通过上面的分析,完全可以利用这种方法替代eval()函数,获取用户的数字数据,这 特别有用,而且降低了“代码注入”攻击的风险。下面是零钱计数程序的一个改进版本: #3_2 new_change.py #A program to calculate the value of some change in dollars def main(): print("Change Counter") print() print("请输入你的各种硬币个数") yuan = int(input("有多少1 元的硬币: ")) fifty_cents = int(input("有多少5 角的硬币: ")) twenty_cents = int(input("有多少2 角的硬币: ")) ten_cents = int(input("有多少1 角的硬币: ")) total =yuan * 1.0 + fifty_cents * .50 + twenty_cents * .20 + ten_cents * .10 print() print("你拥有的硬币总额是", total) main() 注意:在input语句中使用的是int()函数而不是eval()函数,可以确保用户只能输 入有效的整数。任何非法(非整数)输入都会导致程序崩溃和错误消息,从而避免代码注 入攻击的风险。另外,还有一个好处是,这个版本的程序强调输入应该是整数。 使用数字类型转换代替eval()函数的唯一的缺点是,它不支持同时输入(即不能在单 个输入中获取多个值)。 39 Python 程序设计基础 >>> #simultaneous input using eval >>> x,y = eval(input("Enter (x,y): ")) Enter (x,y): 3,4 >>> x 3> >> y 4> >> #does not work with float >>> x,y = float(input("Enter (x,y): ")) Enter (x,y): 3,4 Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: could not convert string to float: '3,4' 这样代价很小,但换来了额外的安全性。在第5章,你将学习如何克服这个限制。作 为一种良好的实践,在编写程序时,应该尽可能使用适当的类型转换函数去代替eval() 函数。 3.3 使用math库 Python标准库math提供了很多有用的数学函数,利用math库可以使用浮点值完 成复杂的数学运算,包括三角函数运算、对数运算等。库就是一个模块,包含了很多封装 好的函数表达式,可以极大地方便程序员解决问题,如利用math库来解决二次函数 问题。在 数学中学习过,一元二次方程的标准式为ax2+bx+c=0,求该方程的解,可以利 用一元二次方程求解公式来进行求解: x=-b± b2-4ac 2a 这里可以利用math库来完成该方程的求解。首先要求用户输入方程中参数a、b、c 的值,最后通过程序处理输出方程的两个解。 #3_3 math.py #A program to math import math def main(): print("求解一元二次方程") print() a = float(input("输入系数a: ")) b = float(input("输入系数b: ")) 40 第3 章 数值计算 c = float(input("输入系数c: ")) discRoot = math.sqrt(b * b - 4 * a * c) root1 = (-b + discRoot) / (2 * a) root2 = (-b - discRoot) / (2 * a) print() print("方程的解为:", root1, root2 ) main() 该程序调用了math库中的sqrt()函数来求平方根。但这个程序仍然存在一个问 题,大家都知道求解一元二次方程时,只有在b2-4ac≥0时方程有实数解,否则方程无实 数解。所以当我们输入的系数使得b2-4ac<0时,会导致程序崩溃。如何解决这个问 题,大家可以思考一下。 熟练利用各种库,可以将复杂代码简单化,简化编写步骤,增强可读性。Python的标 准库math还提供了很多的函数,如表3-3所示。 表3-3 math库的一些函数 Python的函数数 学描 述 sqrt(x) x x的平方根 sin(x) sin(x) x的正弦值 cos(x) cos(x) x的余弦值 tan(x) tan(x) x的正切值 asin(x) arcsin(x) x的反正弦 acos(x) arccos(x) x的反余弦 atan(x) arctan(x) x的反正切 log(x) ln(x) x的自然对数 log10(x) lgx x的常用对数 exp(x) ex e的x次方 ceil(x) [x] 最小的大于或等于x的值 floor(x) [x] 最大的小于或等于x的值 3.4 累积结果:阶乘 在数学中,一个正整数的阶乘是所有小于或等于该数的正整数的积,且0的阶乘为 1,自然数的阶乘记为n!。 大家肯定遇到过这样的问题,6个人随便站队排列,求一共有多少种可能性? 这是一 41 Python 程序设计基础 个排列组合问题,可以很快地给出答案,一共有6! 种排列组合,更详细一点为6×5×4× 3×2×1=720种排列方式。 编写一个程序,让计算机来处理用户输入数字的阶乘。程序的基本结构遵循“输入、 处理、输出”模式: ※ ----伪代码---- ※ 输入要计算阶乘的数,n ※ 计算n 的阶乘,fact ※ 输出fact 整个过程的难点在第二步求解n的阶乘。为了求解阶乘,先来看看阶乘n的表达式: n!=n×(n-1)×(n-2)×...×2×1。从表达式可以看到,每次相乘的数都比前一个数 小1,且整个过程是一个重复相乘(即累乘)的过程,所以,可以使用循环结构来实现这个 过程。因此得到一般模式如下: ※ ----伪代码---- ※ 初始化累加器变量 ※ 循环直到得到最终结果 ※ 更新累加器变量的值 前面已经学习了range()函数,range(n)产生一个数字序列,从0开始,增长到n,但 不包括n。range还有一些其他调用方式,可用于产生不同的序列。可以利用range()函 数的两个参数,即range(start,n),产生一个以值start开始的序列,增长到n,但不包括n。 range()函数还有3个参数的使用方法,如range(start,n,step),这个形式十分类似于双 参数,它的第3个参数step的作用是作为每个数字之间的增量,即前后两个数字的差值。 利用range()函数可以得到其中一种求解阶乘的方法,如下: #3_4 factorial.py #A program to factorial def main(): n = int(input("请输入n:")) fact = 1 for i in range(1,n+1): fact = fact * i print( n,"的阶乘为",fact) main() 当然,求解阶乘还有很多别的方法,这只是其中的一种。有兴趣的话,读者可以自己 再设计几种实现阶乘求解的程序。 42 第3 章 数值计算 本章小结 本章介绍了数字的计算,主要是整型和浮点型数据的计算。对于数值类型的转换和 运用,要注意的是,Python一般会把整型转换为浮点型进行计算。内置的round()函数能 将数字四舍五入到最近的整数值。本章还举例说明了如何实现阶乘的运算。除此之外, 列举了一些math库的函数,方便进行一些数学运算。 知识扩展:运算符优先级 优先级和结合性是Python表达式中比较重要的两个概念,它们决定了先执行表达 式中的哪一部分。Python支持几十种运算符,被划分成将近二十个优先级,只有相同优 先级别的运算符才遵循从左到右计算,否则优先级高的运算符优先计算,运算符优先级如 表3-4所示。 表3-4 运算符优先级 Python运算符运算符说明优 先 级结 合 性 () 小括号19 无 x[i]或x[i1:i2[:i3]] 索引运算符18 左 x.attribute 属性访问17 左 ** 乘方16 右 ~ 按位取反15 右 +(正号)、-(负号) 符号运算符14 右 *、/、//、% 乘除13 左 +、- 加减12 左 > > 、< < 位移11 左 & 按位与10 右 ^ 按位异或9 左 | 按位或8 左 ==、!=、>、>=、<、<= 比较运算符7 左 is、isnot is运算符6 左 in、notin in运算符5 左 not 逻辑非4 右 and 逻辑与3 左 or 逻辑或2 左 exp1,exp2 逗号运算符1 左 43 课程思政:创造了国产软件的骄傲———求伯君 金山办公于2019年11月18日,正式在上交所科创板挂牌交易,股票简称“金山办 公”,市值超过600亿。其主打产品WPSOfice,大家不会陌生。正如金山集团董事长、 小米集团董事长兼CEO 雷军所说:“金山WPS是一家有梦想、有使命感的公司。31 年 坚持做一件事,并且越做越好! ” 回顾WPS的成长之路,不得不提一个人,他就是WPS创作者、金山软件创始人求伯 君。求伯君是金山软件股份有限公司创始人之一,有“中国第一程序员”之称。他是个名 副其实的学霸,高考以数学满分、县里第一的成绩考上了国防科技大学。 1.天赋与钻研劲并存 毕业后的一天,求伯君有个同学的打印机出问题了,请他过去帮忙。求伯君发现故障 原因是打印机驱动不兼容。他的钻研劲儿上来了,认真思考着:“我为什么不搞个通用 的打印机驱动呢?”于是他用了9个晚上,写了一个5万行汇编程序语言的支持多种打印 机的驱动程序,把打印机驱动的问题解决了。同学建议他去四通公司,四通公司的人看见 求伯君的打印驱动以后,马上提出要买下这个程序的全部版权。后来,求伯君就留在了四 通公司。同样的情况再次出现了,四通公司的合作伙伴金山公司的老板张旋龙,有一批机 器的输入输出系统出现了问题,计算机无法启动。他手下团队里的那些专业人士都对这 个问题一筹莫展,四通公司就派了求伯君去试试看,求伯君只花了一晚上就把问题给解 决了。 2.办公软件WPS1. 0横空出世 到了金山以后,求伯君打算做一件大事,他觉得当时市面上主流的汉字处理系统 WordStar不好用,想写一个更好的出来。于是,求伯君对着一台386计算机开始埋头苦 写代码,只要醒着,就不停地写;困得看不清计算机屏幕了,才躺下来眯一会儿。有时忙得 两三天才记起来吃一顿饭。就这样敲了一年零四个月的代码后,求伯君愣是一个人敲完 了122000行代码,WPS1. 0横空出世 ! WPS的诞生具有跨时代的意义,它极大地提升了中文办公的效率。短短的时间内就 成了中国计算机的标配,占据了90%的中国市场。求伯君也因此名扬四海,25岁的求伯 君被人们称为“中国第一程序员”。 3.金山陷入危机 1996年,微软公司希望金山公司将WPS格式与微软公司共享,使两者可以兼容。当 时WPS只有DOS系统版本,微软公司得到兼容格式后,快速将WPS 的老用户转移到 Windows平台,抢占了中国的市场份额,整个金山公司岌岌可危,求伯君于是准备用3年 时间重写WPS 。因为亏损,没有资金,求伯君二话没说就把自己的房子卖了。在市场上 Python 程序设计基础 沉默了几年之后,金山公司推出了WPS97,凭借以往的用户基础,两个月内就售出了 13000多套。 4.中国第一代程序员心中国产软件的骄傲 在求伯君的身上集结着很多的第一:1988年成功开发国内第一套文字处理软件 WPS1.0;1995年推出中国第一个游戏软件《中关村启示录》等。值得一提的是,面对微软 公司的Ofice在国内一统江湖的竞争局面,WPS2005实现了与Ofice在内容和格式上 的“深度兼容”,满足了用户在使用习惯上的要求,而整个软件采用开发式架构,更有利于 对未来各种格式的升级。多年来肩扛民族软件大旗的金山公司宣称,WPS2005的发布 意味着Ofice和WPS之战,将由战略相持阶段转向WPS全面反攻阶段。2011年11月 18日,求伯君宣布退休。现如今,他已经不再插手繁杂的事务。但是,WPS始终是求伯 君年少时梦想的化身,始终是中国第一代程序员心中国产软件的骄傲。 也许从世俗的名利上来讲,求伯君没有比尔·盖茨改变世界的野心,没有登上福布 斯,没有上名人榜。这些我们以为的成功,他似乎从来都没有在乎过。求伯君被称为“中 国第一程序员”,不是因为他熬得了写程序的苦,也不是因为他写代码的能力强大到无人 可及,更多的是他在那个中国处处被国外卡脖子的年代,让我们自己的软件,一直站着,没 有跪下! 第 3 章数值计算 54