从零开始学习PYTHON3讲义(十一)

计算器升级啦

(内容需要,本讲中再次使用了大量在线公式,如果因为转帖网站不支持公式无法显示的情况,欢迎访问原始博客。)

《从零开始PYTHON3》第十一讲

第二讲的时候,我们通过Python的交互模式来入门Python基本知识。当时把Python当成了一个计算器使用。随后从第三讲开始,一直到第十讲,我们进入了编程的方式,并且不断的深入,到第九讲,我们已经完成了Python基本语言、语法部分的学习。

每一讲都有大量的编程练习,估计大家也累了,这一讲休息一下,我们回到把Python当做计算器的状态。当然内容还是要更深入一些,介绍一些常用的高级数学运算功能。


Python的标准数学库

标准库、内置库、官方库这些词其实说的都是一个意思,就是这个库来自Python官方开发团队,随开发语言一同安装无需另外下载的库。

第二讲的时候我们已经发现了,Python本身似乎只能做一些简单的数学运算,加、减、乘、除、乘方。随后整数运算还额外有取余数、整除等几个特别的运算。实际上Python更复杂的数学运算都在标准数学库math之中。一直到第九讲我们介绍了“库”的概念,我们才能更多的介绍Python更高级的计算能力。

如同上一讲说到sys库的那样,我们也可以使用Python的内部帮助来查看math库的详细情况:

>>> import math
>>> dir(math)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'pi', 'pow', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc']
>>> help(math)
...   #将有大量详细的帮助信息,这里略去

下面我们介绍一些常用的math内置数学函数:

函数 功能
math.pi 数学常数π= 3.141592……
math.e 数学常数e = 2.718281….
math.tau 数学常数τ= 6.283185……
math.ceil(x) 返回x的上限,返回最小的整数A (A>=x)。如math.ceil(3.14)返回的整数为4官网math库
math.fabs(x) 返回绝对值x。
math.factorial(x) 返回 x!。如果x不是积分或者是负的,就会产生ValueError。
math.floor(x) 返回x的下限,返回一个值最大整数A (A<=x)。如math.floor(3.14)返回的整数为3
math.exp(x) 返回 ex也就是 math.e ** x
math.pow(x,y) 返回x的y次方,即返回 x ** y
math.sqrt(x) 返回 \(\sqrt x\)
math.degrees(x) 将角x从弧度转换成角度。
math.radians(x) 把角x从度转换成弧度。
math.acos(x) 返回 x 的反余弦
math.asin(x) 返回 x 的反正弦。
math.atan(x) 返回 x 的反正切。
math.cos(x) 返回 x 的余弦。
math.sin(x) 返回 x 的正弦。
math.tan(x) 返回 x 的正切。
math.log(x,a) 返回 \(log\;a^x\),若不提供a参数,默认使用e

有了这些函数的帮助,我们一下从小学水平上升到了高中:),来看几个使用的例子:

>>> import math	#所有math的函数,使用之前必须引入库,引入一次即可
>>> math.sin(1)    #1的正弦
0.8414709848078965
>>> math.pi 	#π常量
3.141592653589793
>>> math.sqrt(3) #计算3的平方根
1.7320508075688772
>>> 

扩展库和各种函数的学习,通常不需要你一次都记住,而是用的时候查资料会用即可。常用的函数,用的多的自然就记住了。
随用随查资料这种形式,不同于以前的课堂笔记,一般都是用网页书签来记录下来常用的资料地址,这样才能快速的查询。比如math库的部分常用函数的中文资料:https://www.cnblogs.com/renpingsheng/p/7171950.html
中文资料一般都更新慢一些,并且通常不是很完整,官方的英文资料则更快,但是需要你能阅读英文。立志希望从事信息技术行业的同学,英语的学习也要同时加强。官方英文资料地址:https://docs.python.org/3/library/math.html


第三方数学库numpy

“第三方”是在计算机行业中很常用的概念,指的既不是开发者官方提供的,也不是用户自己开发的。是由其它组织开发并提供服务的内容。可以把两者做一个比较:

标准库 第三方扩展库
同为软件库,相同的使用方法 同为软件库,相同的使用方法
由PYTHON官方或认可的开发团队开发维护 通常由世界范围内许多不同公司或组织开发维护
通常只有一个最稳定的版本 同一个功能,可能有很多个团队的不同产品,质量参差不齐
主要完成常用、基本、必备功能 解决各种各样问题
随PYTHON安装,直接就可以使用,称为标准库 需要额外安装,跟不同操作系统可能还有兼容性问题,称为第三方扩展库
开发规范、命名习惯基本统一 各自有各自的标准、规范,互相之间有可能习惯差别很大

通常能生存并传播很广泛的第三方扩展库都有惊人强大的功能。在享受这些“超级”功能的同时,每个第三方扩展库都需要安装之后才能被Python程序“引用”和“使用”,是第三方扩展库最大的障碍。
为此Python发展出了很多扩展库的管理工具来帮助开发人员安装、管理、删除扩展库。我们第一讲介绍了使用最多的pip管理工具。
使用pip管理工具安装numpy数学库的方法如下:

#在Windows中,首先退出当前的Python软件
#使用管理员模式执行cmd命令行,然后执行如下命令:
pip install numpy
#某些windows系统需要使用pip3
pip3 install numpy

#linux和mac在命令行执行:
sudo pip3 install numpy

使用习惯之后,这样一行的安装命令根本不会对你使用扩展库有什么影响,而且只需要安装一次,不换电脑就可以一直使用。

numpy的使用跟math的使用几乎是相同的,但是相较于只有50多个预置数学函数的math,numpy包含了600多个。只要跟数学相关的,几乎所有需要用到的函数和常量都已经有了。我们举几个例子:

#首先使用之前一样是必须先引用
import numpy as np
    #as np表示引入后使用np的名字来调用,这样每次都可以少敲几个字母

np.sin(1)    #正弦函数
 => 0.8414709848078965
np.pi    #π常量
 => 3.141592653589793
np.sqrt(3)  #平方根
 => 1.7320508075688772
np.arccos(0)   #反余弦函数  
 => 1.5707963267948966

#查看帮助
help(np)       #第一次帮助会从网上获取,速度比较慢

第九讲我们曾经讲过了使用列表类型保存矩阵的方式,可惜就基本Python的功能来讲,也只是能保存而已,想要计算,需要自己使用复杂的循环嵌套来完成。但矩阵运算在numpy是直接内置的,比如: \(A = \left\{ \begin{matrix} 2 & 3 & 4 \\ 5 & 6 & 7 \\ 8 & 9 & 10 \end{matrix} \right\} \times 3\tag{1}\)

我们直接看numpy的计算方式:

>>> np.array([[2,3,4],[5,6,7],[8,9,10]])*3
array([[ 6,  9, 12],
       [15, 18, 21],
       [24, 27, 30]])

np.array函数,实际是numpy中的列表类型。列表的定义跟标准Python很像,是用嵌套的“[]”完成的。随后numpy的类型直接就支持矩阵乘法,所以最后“*3”。执行后输出了矩阵的计算结果。对比的如果使用标准的Python,肯定要使用两个循环嵌套,然后逐项的进行乘法计算。速度会慢很多,编程也复杂很多。

再比较一个例子。第六讲中我们讲了range函数,是跟for循环一起介绍的,大家应当不陌生。当时重点说明了range返回的是一个整数的序列类型,那碰到需要使用小数的序列类型的时候怎么办呢?通常的办法只能在循环体中增加一次整数同浮点小数的乘法运算来生成每次循环使用的小数。比如:

step=0.11
r=[]
for i in range(10):
    r.append(i * step)
#结果为:
[0.0, 0.11, 0.22, 0.33, 0.44, 0.55, 0.66, 0.77, 0.88, 0.99]

而numpy中,直接有支持小数序列的类型:

#Python内置的range函数
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

#numpy中支持小数序列的linspace
>>> np.linspace(1,2,10)
array([1.        , 1.11111111, 1.22222222, 1.33333333, 1.44444444,
       1.55555556, 1.66666667, 1.77777778, 1.88888889, 2.        ])

linspace函数的三个参数跟range函数区别比较大,需要注意:第一个参数是指起始数值;第二个参数是指结束数值,注意这里会包含结束数值,而range中是不包含结束数值;第三个参数是指从开始到结束,分为多少份,也就是最后序列的长度。

我们至今所看到的Python数学计算,都属于数值计算的范畴。所谓“数值计算”就是指不管计算过程多么复杂,最终以数值的形式得出计算结果。
数值计算在实际应用中使用的最多,但缺陷也比较明显。比如从上面linspace的例子就能看出来,看起来所生成的浮点小数序列,并不是很整齐,几乎可以确定有被省略的部分。计算机内部的存储是2进制数,我们平常习惯的计算方式是10进制数,两者之间的转换会有误差,无理数的多次截取也会造成误差。我们可以再举一个更明显的例子:

import math
math.sqrt(8)
结果2.8284271247461903
math.sqrt(8)*math.sqrt(8)
结果8.000000000000002

上例中,因为对8开平方的时候数据做了截取,相乘计算回平方值之后,无法做到精确的得出8,只是一个很近似的值。这在存在大量计算而精度又要求比较高的情况下,仔细的考虑化简的时机和计算的方法将会耗费大量的精力。
这种情况在第三方的numpy数学库中同样是存在的:

import numpy as np
np.sqrt(8)
结果2.8284271247461903
np.sqrt(8)*np.sqrt(8)
结果8.000000000000002

实际上只要是使用数值计算就会出现这种情况,尚无法避免。
为了应对这种方式,在数学中大量采用了符号计算。我们目前数学课上学到的方程式、多项式基本都属于这个范畴。往往并不需要求出最终的计算结果。化简到一些包含简单符号和算式的结果就可以满足应用。因此符号计算在科研、工程领域都有广泛应用。

Python有一个第三方的符号计算扩展库,名为sympy。安装方式为(以后的安装介绍均以windows为例,不再介绍linux及mac,相信参考windows的方法,在linux和mac安装都不应当有问题):

#首先使用管理员模式打开cmd命令行,然后执行:
pip install sympy  
#实际上pip工具可以同时安装多个扩展库,比如:
pip install numpy sympy

相对熟悉的数值计算,sympy符号计算库理解起来会难一些。Sympy试图建立一整套运算体系,对每次的结果进行符号计算,尽力保持计算的精确度。试图建立一整套体系的原因是这样:在Python中,加、减、乘、除包括等号等等所有字符,基本都已经有了默认的功能,比如通常的数学数值计算。
我们前面也讲过了,这些符号本身属于保留字的一种,是不能被我们用于其它用途的。因此在不会歧义的位置,会继续使用原有计算符和函数,有歧义的位置,需要使用Sympy自己的函数,比如分数函数Rational(稍后会有讲解)。
只要算式会被化简从而成为小数的情况,都应当考虑使用Sympy自己的函数,通常都是分数、除法、数学函数的位置,否则就等于使用了原有的数值计算,可能导致精度降低。

sympy的使用方法,先来看一个例子:

#使用内置的数学库
import math
math.sqrt(8)
结果2.8284271247461903

3*5*math.sin(7) #numpy.sin(7)也是相同的
结果9.854798980781837

#下面使用sympy
import sympy
sympy.sqrt(8)
结果2*sqrt(2)   #注意结果的样式

3*5*sympy.sin(7)
结果15*sin(7)   #注意结果

乍看起来,sympy的结果似乎很怪异。其实如果把计算机函数翻译为数学函数,这个结果非常类似我们学习数学的时候公式化间的结果。继续看示例:

import sympy

#平方根
sympy.sqrt(8)
结果2*sqrt(2)

sympy.sqrt(8)*sympy.sqrt(8)
结果8

2*sympy.sqrt(2)
结果2*sqrt(2)

#正弦函数
sympy.sin(1)
结果sin(1)

#π常数
sympy.pi
结果pi

#分数
sympy.Rational(1,2)
结果1/2

注意上面的计算结果,都没有前缀的sympy,而是直接的sin/sqrt这样的结果。这说明,其实sympy使用的时候,最好使用from sympy import *,还记得吗?这相当于从sympy把所有可用资源都导入到了当前文件作用域,因此调用的时候可以完全省略sympy前缀。
继续说符号计算。上面使用的例子,你会发现使用符号计算的方法,因为可能会变成无理数的部分都使用了符号或者公式来表达了。所以两个平方根相乘这样的运算,是可以精确还原到原始值的。

既然是符号计算,直接使用符号量在数学表达式中也是很有特色的功能:

#符号声明
#在第二讲说变量的时候,
#我们特别说明变量是“已知数”
#这里创建的符号变量,其实就是
#代表数学公式中的未知数
#当然最后这个未知数,还是使用Python变量来表示的,
#sympy.Symbol就是一个sympy库中的类型。
x = sympy.Symbol('x')   #定义未知数x
y = sympy.Symbol('y')	#未知数y
m,n,z = sympy.symbols('m n z') #同时定义多个未知数

#以下是使用定义的未知数,进行带未知数的数学符号计算
m*x*3+8
结果3*m*x + 8

(x+y)*3
结果3*x + 3*y

再强调一下,在sympy中定义的未知数类型,变量的确是Python的变量。所代表的含义可是sympy符号计算中的未知数,而不是我们常见的Python变量。


挑战

下面我们利用强大的符号计算来进行一个多项式的化简: \((x + (2xy)^\frac{1}{2}+y)(x - (2xy)^\frac{1}{2}+y)\)
建议你自己动手化简一下,虽然我们不是数学课,但数学技能还是很重要的。 随后你应当能得到正确答案:
\(= x^2 + y^2\)
上面是手工来化简的结果。下面到了让sympy上场的时间了:

#引入扩展库
from sympy import *

#定义x/y两个符号
x,y = symbols("x y")

#化简函数simplify()
print(simplify((x+(2*x*y)**Rational(1, 2)+y)*(x-(2*x*y)**Rational(1, 2)+y)))
#执行结果
 x**2 + y**2

其实同第二章一样,这一章的难度,同样是要用Python语言来描述数学公式。上例中的simplify函数式sympy中的一个函数,表示把参数当做数学表达式,然后进行化简操作。加法、乘法、乘方都不会造成小数,也没有语法上的歧义,所以直接使用了标准的数学运算符。1/2这种除法会有可能导致小数,从而有二进制到十进制转换的误差风险;并且1/2会直接使用数值计算,会导致算式过快的求值,导致最后化简失败,所以这里使用sympy内置的分数函数Rational,这个函数有两个参数,分别代表分子和分母。
经过这些解释,你是不是能看懂了?最后看化简的结果,跟我们手工的过程一模一样。这些新的函数,希望你自己给自己找一些算式多练习,才能更快的掌握。


解方程

解方程在数学中简直占了半壁江山啊。我们仍然从第二讲的老例子开始:

甲、乙两人相距36千米,相向而行,如果甲比乙先走2小时,那么他们在乙出发2.5小时后相遇;如果乙比甲先走2小时,那么他们在甲出发3小时后相遇,甲、乙两人每小时各走多少千米?(假设甲乙的速度均匀稳定)

希望你还记得原来的结题过程,我直接列出来吧,毕竟我们要学习的是Python不是数学:

  • 假设甲的速度为x千米/小时,假设乙的速度为y千米/小时
  • 列方程式(2.5+2)x+2.5y=36,3x+(3+2)y=36
  • 根据方程2推导为:x=(36-5y)/3,代入方程1
  • y=(124.5-36)/(4.55/3-2.5)
  • 最后得:y=3.6,x=6

解方程首先的问题是,“= ”已经被用作了赋值操作,跟前面/的原因一样,不能直接用来描述等式。不然Python会直接报错。

sympy定义了sympy.Eq()函数来描述等式,以上面的两个方程为例,可以写成这个样子:sympy.Eq((2.5+2) * x+2.5 * y,36)sympy.Eq(3 * x+(3+2) * y,36)。逗号隔开的,就是等式两端。其它的注意事项,跟上面“化简”的时候讲的一样。下面看看我们解方程的过程:

#引入扩展库
from sympy import *

#定义两个未知数符号
x=Symbol('x')
y=Symbol('y')

#定义两个等式
a = Eq((2.5+2)*x+2.5*y,36)
b = Eq(3*x+(3+2)*y,36)

#使用sympy.solve函数解方程组
print(solve([a,b],[x,y]))

#运行结果:
{x: 6.00000000000000, y: 3.60000000000000}

嗯,说不编程序了,实际最后还是编了,好在比较简单:)
程序中定义未知数符号、描述等式,重点是使用了sympy.solve函数来解方程。函数接受两个参数,两个参数都是列表。第一个列表中是方程式(等式),第二个列表是要求解的未知数。

我们再把程序简化一下:

#引入扩展库
from sympy import *

#在一行中直接定义两个未知数符号
x,y = symbols("x y")

#使用sympy.solve函数解方程组
solve([Eq((2.5+2)*x+2.5*y,36),Eq(3*x+(3+2)*y,36)],[x,y])

结果{x: 6.00000000000000, y: 3.60000000000000}

这样看起来更清楚了。有没有觉得sympy符号计算很强大?


练习时间

  1. 使用symbol解方程组:
    \(\begin{equation} \begin{cases} 2x-y=5 \\ 3x+4y=2 \end{cases} \end{equation}\)

  2. 化简表达式:
    \(\frac {\sin(60+\theta)+\cos(120)\sin(\theta)}{ cos(\theta)}\)
    这道题有一些提示:

    \(\theta\) 在Python中很难输入,可以使用一个未知数x代替,不影响结果。

    Python的数学库只接受\(\pi\)角度,也既我们习惯的180度,所以题目中的60度可以表示为\(\pi/3\);120度则表示为\(\pi/3*2\)。

    式子中的分子、分母因为都有未知数,不会引起即时计算影响计算结果,也不会有歧义,所以就是用“/”计算符即可,不用使用Rational函数。

作为一门编程课,本讲并未提供太多的数学算式来帮助你记忆新学的数学函数。建议你从自己的数学学习中寻找一些算式来多做一些练习。


本讲小结

  • 复杂的数学计算是理工科必备的基本能力,也是最频繁需要的
  • 数学计算在计算机应用中,分为求得结果的数值计算及公式化简为主的符号计算,各有用途,都很重要
  • Python在多种扩展库的帮助下有强大的计算能力
  • 本讲的重点是公式化简、解方程,要学会使用语言中的运算符、函数等来描述数学公式

学习资源

numpy: https://docs.scipy.org/doc/numpy/user/quickstart.html sympy: http://docs.sympy.org/latest/tutorial/index.html


练习答案

  1. solve([Eq(2*x-y,5),Eq(3*x+4*y,2)])
  2. simplify((sin(pi/3+x)+cos(pi/3*2)*sin(x))/cos(x))

最终结果请在Python中试着运行看看,别忘了引入sympy库。