人工智能(高中版)
上QQ阅读APP看书,第一时间看更新

0.2 编程基础

0.2.1 起步

一般来说,在计算机上的任务都是通过执行一系列预设的计算步骤来实现。其中,编程人员通过编程语言描述计算步骤(2),而计算机机械地执行计算步骤,从而得到计算结果。用编程语言描述的计算步骤称为程序,程序中的每个计算步骤被称为指令。

Python是目前比较流行的一门编程语言,其有语法语义直观、支持交互式运行和脚本运行、支持的库类型丰富等特性。Python近年来被广泛地应用于包括生产和科研在内的各种场景中。本书的课程网站提供了相关的电子资料,请读者到课程网站下载相应软件,搭建练习环境。作为起步,我们先打开Python的交互式运行界面,尝试执行一些Python程序。

例0.6:在屏幕输出“hello,world!”

在此例子中,python表示进入Python的交互式运行界面;>>>表示命令行可以接受指令;print()是一条指令,指挥计算机在屏幕输出其括号里的内容;而在“hello,world!”中,双引号表示这个整体是一个字符串(而不是程序),引号内是这个字符串的内容。程序运行结果在下一行,我们看到屏幕上输出了“hello,world!”(3)

例0.7:1至5求和

这里将Python当作计算器使用,先采用和式1+2+3+4+5来求和,可见Python能够计算该表达式的值,并输出结果。

下面,看一个辗转相除的例子。辗转相除的算法描述如下:用两个数中较大的数除以较小的数得到余数,如果余数为0,那么除数为最大公约数,如果余数不为0,那么取除数和余数,重复上述过程。

例0.8:辗转相除法求91和105最大公约数

在本例中,程序运行如下105%91得到14,其中,%表示整数相除并取余数为结果,105除以91余数为14(商为1)。余数不为0,所以,重复上面步骤91%14得到7;余数7仍然不为0,重复计算步骤14%7得到0,此时余数为0,那么最大公约数为此时的除数7。

0.2.2 值的类型和算术运算

Python每个计算步骤中的值是有类型的。在此前的例子中,“hello,world!”的类型是字符串;而“1+2”中1和2的类型是整数。在算术运算中,Python中的数值类型有整数和浮点数。浮点数包括整数和一定精度内的有限小数。常用的算术运算包括加、减、乘、除、乘方、开方、取模等,它们的符号叫做算术运算符(例如+,-,*,/,等)。

例0.9:浮点数加减乘除

这里计算了1.5与2.1的加、减、乘、除。在乘法中,由于计算机内部二进制表示的问题,无法精准地得到3.15,而是有一定误差,但是这种误差通常比较小(本例子中为10-15),不影响使用。另外,“//”的结果是保留商的整数部分。

不同类型的值之间可以通过Python内置的类型转化函数实现相互转化。从低精度的类型转化为高精度的类型,数值不会损失精度;但一个值从高精度类型转化为低精度类型,则可能需要截取低精度部分并舍弃高精度部分。例如,int()会将浮点数的整数部分返回并舍弃小数部分。

例0.10:类型转换

0.2.3 变量、表达式、赋值

在编程语言中,我们用一个标签指代一个值,并将标签称为变量。变量可以理解为一个名字,该名字代表一个值。变量有命名规则,请参考附录。单独的变量、单独的值或者用运算符连接的变量和值称为表达式。表达式有算术表达式、关系表达式、布尔表达式等,请参考附录。计算表达式的值时,Python用变量的值替换它的出现,进行计算。一个表达式中可能由多个运算符连接多个值或者变量,此时,计算表达式的值时,需要按照运算符之间的优先级顺序来进行。在常见的算术表达式中,小括号优先级最高,然后依次是乘方和开方、乘除和取模、加减法。

变量的更新是通过赋值语句完成的。赋值语句由左值、赋值运算符和右值组成:左值必须是一个变量,赋值运算符是“=”,右值是一个表达式。Python运行中遇到赋值语句,会先计算右值表达式的值,再将左值变量的值更新为表达式的值(4)

例0.11:变量赋值与计算

在例0.11中,a=1为赋值语句,其作用是将1赋值给a;赋值表达式b=a+2中,a的值为1,a+2的计算结果被赋值给变量b;最后打印ab的结果输出其值。一个变量在Python程序中第一次出现时必须赋值,也叫做变量的声明(反例见第7行,程序报错,无法执行)。表达式中不允许使用未加声明的变量(反例见第11行,程序报错,无法执行)。

例0.12:用变量1~5求和

用变量a代表“中间结果”。先用a来存储1+2的中间结果(a=1+2),然后在变量a上逐个叠加每个加数。a=a+3中,右值表达式a+3中a的值(3)被用于计算,计算结果6被赋值给左值变量a。以此类推,Python计算a=a+4,a=a+5。最后通过print()来输出最终a存储的值,输出结果为15。

对比例0.7和例0.12,在交互式计算中,我们输入表达式后得到结果,并用于下一步计算,这种计算方式叫做交互式计算;使用变量后,无需等待计算结果,而是可以用含有变量的指令描述每一个计算步骤,然后交给Python一次执行。减少编程人员的互动参与可以大大提高编程与计算的效率。因此,在本节之后,将引入Python的脚本执行:即先将整个计算流程描述为程序,再执行整个程序得到结果。

例0.13:1~5求和的脚本执行

在本例中,sum.py包含了前5行的代码。首先用cat命令展示了脚本内容,然后通过python sum.py来执行脚本,最后程序结果输出到屏幕上。

在下面的介绍中,默认用example.py表示编写好的脚本代码。

0.2.4 控制流

在例0.12中,所有赋值语句按照行号从小到大依次排列,Python在执行过程中,也按照相同的顺序来执行,称作顺序执行。但是,在一些场景和程序中,我们希望表达非顺序执行的语义。例如,在例0.8的辗转相除中,我们想表达“如果余数为0,则除数为最大公约数;否则,取除数和余数继续相除”。Python提供了分支和循环两种语句来表达非顺序执行的语义。

在这两种结构中,需要表达“某条件为真/假”。这种有真假值的条件是通过布尔表达式来完成的。布尔表达式可以是简单的关系表达式,例如大于(>)、小于(<)、隶属于(in)等,也可以是关系表达式的逻辑组合,例如与(and)、或(or)、非(not)等;每一个布尔表达式的值必定是真(True)或者假(False)。其定义见附录。

分支语句允许程序非顺序执行,其代码结构如图0.4所示。程序中的关键词为if、else和:(英文冒号)。第一行“if布尔表达式:”为if头部。其语义是如果布尔表达式为真(条件成立),则执行代码块A;否则,执行代码块B。其中代码块A和代码块B均为子程序,其结构与一段程序相同。

图0.4 条件分支程序结构

注意,图0.4中还有一段代码块C,C不同于代码块A和B。两者形式上的不同在于,代码块A和B以空白符(空格或者制表符)开头,起始位置较if、else和代码块C要靠后,这种格式称为缩进;而代码块C与if和else起始位置相同。缩进的意义在于描述程序的逻辑结构,图中程序表示“如果条件成立执行A,条件不成立执行B;然后执行C”。如果C部分的缩进与B相同(或者假设Python没有缩进),那么运行时,计算机无法将上述逻辑与“条件成立时执行A,条件不成立时执行B和C”区分开来。

例0.14:分支语句(判断整除、赋值、缩进)

第9行print()内有两个变量,print允许输入多个表达式,并用逗号隔开,运行时它们会被逐个打印到屏幕上,相邻结果之间用一个空格隔离。

如果需要表达“当判断条件不成立时无需执行任何动作”,那么else及其else分支可以省略。如果需要连续进行判断,则可以通过使用elif来简化程序结构的写法。例如,“如果条件1成立,执行A;否则,如果条件2成立,执行B;否则,如果条件3成立,执行C;否则,执行D”可被表达为图0.5中的结构。

图0.5 多层条件分支程序结构

循环语句有两种表达方法:while循环和for循环,它们均由循环头部和循环体组成。在循环体执行结束后,程序会跳转到循环头部再次执行。图0.6展示了while循环,当代码执行到循环头部时,先判断条件语句A是否成立:如果成立,则执行循环体代码B;否则,将跳过循环体执行之后的代码C。如果代码块B得到执行,那么执行结束后,程序会再次跳转到while头部,判断条件A是否成立,并依此决定执行B或者跳过B执行C。

图0.6 while循环程序结构

例0.15:while循环,1~5求和

在此段程序中,用变量sum存储中间结果,并在累加结束后作为最终结果输出;用i代表每一个加数。进入循环之前,将sum初始化为0,i初始为1。循环的执行条件是i<6(或者等价的i<=5)。程序第一次执行到while头部时,i<6条件成立,循环体被执行,sum+=i(即sum=sum+i)将1累加到sum上,i+=1使i增加1成为2。然后,程序跳回到循环头部再次执行,如果条件成立,sum累加ii递增1。如此反复5次之后,i的值成为6,跳回到循环头部,条件不再成立,循环体被跳过,执行之后的程序,输出此时的sum值为15。

for循环的结构如图0.7所示,由for头部和循环体组成。for头部的关键词是for和in并跟随:(英文冒号),有一个单体变量i和一个容器类的变量C。一个容器类的变量内部可以含有多个单体变量。容器类的变量包括列表、元组、词典、集合等,将在附录中介绍。本节的介绍中仅考虑使用列表。for循环的语义是,每轮从容器C中取一个变量,赋值给变量i(各轮次之间选不同的变量),然后执行循环体B;当B执行完毕之后,返回到for头部,进入下一轮次(即从容器中选取下一个变量执行循环体)。每一轮次中i可以在循环体中使用。

图0.7 for循环程序结构

例0.16:for循环求1~5的和

在此例中,用变量i(5)来遍历容器[1,2,3,4,5]。这里的容器是一个列表(list,用中括号封装若干个单体的元素,在附录中有详细介绍)。每一轮次中,i分别取值1,2,3,4,5,并被累加到sum上。容器遍历结束后,程序打印输出结果sum。

我们引入range()函数(6)来简化生成列表容器的过程。具体来说,可以用range(start,end,step)生成一个列表(7),当start小于end时,生成规则如下:从start开始,列表添加start,添加start+step,添加start+2×step,如此重复至添加最后一个值为start+n×step且小于end的值。

例0.17:1~100求和(for循环、range()函数)

在此例中,用range()直接生成了1~100的列表,再用for循环求和。用range()生成列表与编程人员手写1~100的列表相比,大大提高编程效率。这里补充几点range()函数的使用技巧:step如果不填写,那么默认为1;如果step大于end,且step为负数,那么将按照递减的顺序生成列表;其他情况,都会生成空列表[]。

在一个循环结构中,可在循环体中使用break指令提前终止循环。在循环体执行过程中遇到break时,会直接忽略循环体中break之后的代码,跳转到循环结构之后的代码继续执行。

例0.18:辗转相除

这是一个完整的辗转相除的程序。注意,while循环的条件是True,意味着程序执行到这里一定会执行循环体,只能靠循环体内部的break来跳出循环体。此例中,可以看到分支、循环这种程序结构可以相互嵌套,表达更丰富的语义。在这种嵌套中,也需保持同一层次的子结构缩进长度相同(即内部if头部和while循环体缩进相同),子结构内部保持自己的缩进规则(即if结构中的break比if头部要缩进一段长度)。如果某些循环逻辑不正确,无法跳出循环体(循环头部条件永远为真且循环体内无法执行break),那么就构成一个死循环,导致程序无穷尽的运行下去,不能停机。

在一个循环结构中,可以在循环体中使用continue结束该次循环并进入下一次循环。在循环体执行遇到continue时,循环体内continue之后的代码不会被运行,但是程序会跳转到循环头部继续执行:while循环会再次判断条件是否成立并决定执行或者跳过循环体,for循环会继续去容器内下一个元素执行循环体。

例0.19:求1~100中,所有奇数的和

0.2.5 函数

与大部分高级编程语言相同,Python允许编程者定义函数。函数的作用是封装一部分重复逻辑,在程序中通过调用函数名来重复使用该逻辑。这样做既可以避免重复编写该逻辑,也能让程序结构更加清晰。

函数的结构如图0.8所示,其中,函数头用关键词def定义函数的名称和输入(函数可以没有输入);函数体封装一部分代码,称为函数体,函数体比函数头要缩进一个单位。函数的输入称作形式参数(简称形参),函数体描述处理形式参数的逻辑;函数可以设置输出(也可以没有输出),称作返回值,用关键词return来描述。

函数定义之后并不被执行,而是通过在程序中调用来执行。调用者调用一个函数(也称被调用者)需要描述函数名,并用表达式(包括值、变量和它们的组合)替换函数定义中的形式参数。在这个过程中,输入函数的表达式被称作实际参数(简称实参)。在执行过程中,Python会用实际参数替换形式参数,并执行函数体内部的逻辑。函数体是一段程序,其执行逻辑与此前所述的顺序执行、分支循环无异。当被调用者函数体执行完毕后,会跳转到调用者函数调用指令之后的相邻指令,继续执行。实参的传递方式有两种,请参考附录。

图0.8 函数结构

函数体的执行过程中可能会遇到return关键词。如果return之后没有表达式,那么计算机会立即终止函数执行,跳出函数,回到函数调用指令之后的相邻指令继续执行。如果return之后有表达式,那么计算机会先计算表达式的值作为返回值,然后终止函数执行,跳出函数。此时,调用者可以继续执行下一条单独的指令,也可以把函数调用整体作为表达式去组合更复杂的表达式或者指令(例如,赋值、关系表达式、算术表达式等)。在后者中,函数的返回值将用于替换函数调用位置并参与复杂表达式或者指令的运算。

函数调用并不罕见。例如在例0.6中,使用print()向屏幕打印字符串。这是一个已经Python内置的函数,其函数体逻辑是接收括号内的字符串,并向显示器设备发送消息,使显示器在相应位置排布像素点组成字符串的视觉展示。常见的运算符也可以理解为一个函数,例如,加法就是一个接收两个参数并输出它们和的函数。下面是一个定义和调用函数的例子。

例0.20:一些函数定义和调用Add,IsOdd函数

在下例中,不需要写三遍辗转相除的代码,而是写一遍,封装为函数后,调用三次,实现三次计算。

例0.21:求三对数的最大公约数(91,105),(118,13),(36,192)

前文提到,在函数调用中会用实参替代函数定义中的形参,并在执行过程中用实参进行计算。这里存在一个问题:如果函数体内有指令对形参进行修改(赋值等),那么函数调用过程中,实参对应的变量是否也会被修改呢?答案是要根据实参变量的类型分情况讨论。在本章中,整数和浮点数做实参时均不会被修改。附录中的复杂类型允许被一个函数修改,但是不会被赋值。

在一段程序中,一个变量并不是在任何地方都可以被使用,它可以被使用的范围称作该变量的命名空间。基本原则如下:一个函数内声明的变量仅可以在函数之内使用,称为局部变量;函数外声明的变量既可以在函数内使用,也可以在函数外使用,称为全局变量。当函数内需要使用全局变量时,需要在使用前用“global变量名”来声明该变量。

全局变量可以在程序中的任何位置使用(如果此前没有过声明,则会创建新的全局变量);如果此前在别处已有声明,则两处声明指代同一个变量(各处的修改和读取相互影响)。如果函数内的局部变量与一个全局变量重名(未用“global”声明),函数内的变量是不同于全局变量的另外一个变量。实际上,Python允许函数定义内嵌套另外的函数定义,此时变量命名空间的原则是内层函数可以使用外层函数中声明的变量,本书中不加以详细阐述。

例0.22:变量命名空间示例

一个函数的函数体内可以调用自己本身,这称作递归调用。递归函数可以用于表达“先进行某项操作,再对结果重复相同的操作”这样的语义。例如,例0.23中,在某次计算结束获得商和余数后,“对除数和商重复进行除法计算,并判断余数是否为零”这一语义就可以通过递归调用函数本身来实现。递归调用也是函数的一种,函数实际参数的修改原则与上文所述的针对函数的修改规则相同。

例0.23:求用递归函数求最大公约数

为了结构清晰,实际程序往往由多个文件构成,文件之间的程序的相互引用和调用通过关键词import实现。被引用的程序也称作模块。如下例中,“import module1”表示引入module1中的所有变量和函数。调用者通过module1.foo1()和module1.var1来使用模块中的函数和变量。另外,“from module2 import foo2”表示从模块module1中引入函数foo2(变量也用相同的方式引用(8))。调用者可以更简化地通过foo2()来使用该函数;而“from module3 import*”则将module3中的所有变量和函数引入到被调用者,并可通过简化方式foo3()来使用(9)

例0.24:模块的定义和调用

除了允许使用者自己编写模块之外,Python内置了若干模块,使用者可以直接调用来辅助计算(10),例如第0.2.2节中的类型转换函数就是Python内置函数。同时,整个Python社区也贡献了种类丰富的第三方模块,安装后便可调用。这些封装好供编程人员直接使用的模块也叫做库。本节中将展示部分math库函数(11)

例0.25:内置标准库:求绝对值、最大值、最小值、和

例0.26:math库举例:求最大公约数、平方根、指数、对数、正弦值

0.2.6 输入输出

在编程中,将数据和程序分开是一个好习惯(也称解耦合),有助于改善程序结构和可读性,且有助于运行时调试。程序与数据解耦合则要求在程序运行过程中动态地加载数据,加载数据的接口被称为输入输出函数。下面将介绍两种输入输出方式——控制台输入输出与文件输入输出。

控制台输入指通过键盘输入,控制台输出指通过显示器输出。前文中介绍的print()函数即是通过屏幕输出。print()函数输出一个字符串。如果类型不是字符串,Python内部会调用str()函数进行格式转换,如果str()不支持该类型转换,程序会报错。

通过键盘输入使用input()函数,函数参数中可以有一个字符串来提示输入内容(也可以没有),函数返回值是一个字符串。运行时,参数的字符串打印到屏幕上,用户输入内容;当用户输入回车符号时,input()函数返回用户输入内容(不包括回车符号),程序继续执行。因此,input()通常被赋值给一个变量来接受返回值。

例0.27:辗转相除(控制台输入)

计算机中的文件分为文本文件和二进制文件。文本文件使用人可读的字符编码(例如ASCII码),可用编辑器打开进行编辑;二进制文件用0和1进行编码,一般用于计算机存储数据和运行程序(例如可执行文件、图片、视频等)。二进制文件由于编码方式更高效,在相同信息量的情况下,二进制文件占用的存储空间更小一些,但通常不是人可读的。在本书中,读者编写的程序是文本文件(计算机会将其转化成二进制的形式进行运行),图片数据是二进制文件,自然语言数据使用文本文件存取。

本节中,将以文本文件的读写为例,介绍文件的输入输出。文件在读写之前,需要先打开,并产生一个文件描述符。文件描述符用一个变量存储,对文件的读写操作通过文件描述符进行。打开文件并返回文件描述符的方式是调用fd=open(文件名,模式),其中fd存储了文件描述符,open中的文件名是一个字符串,用来表示需要被打开的文件(12),模式参数也是一个字符串,用来描述文件打开后进行的操作。文本文件打开后的操作分读取、写入和追加,对应的参数是“r”“w”和“a”(13)。文件打开并进行操作之后,需要关闭。这一步通过调用文件描述符的一个函数close()完成。

例0.28是一个文件写入的例子。该程序先打开一个名为hello.txt的文件;再调用fd.write函数写入一个字符串“hello,world!”;最后调用fd.close()关闭文件描述符(14)。open中的模式“w”表示覆盖式的写入,例0.28中的文件第二次被打开后再写入,则会覆盖文件原来的内容。

例0.28:文件写入

下面的例子是一个文件追加的例子,程序打开时使用“a”模式,再调用fd.write会在文件尾部添加新内容。

例0.29:文件追加

文本文件的读取有许多种方式。下例中,将通过“r”模式打开一个文件。此时调用fd.read()会将所有文件内容读出,并以一个字符串的形式返回;调用fd.readlines()会将文件所有内容读出,以一个列表形式返回,列表中每一个元素依次为文件中每一行(以文件中的回车符号分行,且列表中的元素包含行尾的回车);文件打开后,fd内部隐含一个文件指针,指向一个当前位置(15);调用fd.readline()则会返回当前指针之后的一行(以回车结尾,返回值中包含回车),同时,指针向后移至下一行起始位置;再次调用fd.readline()则会返回下一行内容,readline常和循环语句一起使用,用于逐行处理文件。

例0.30:文件读取

用“w”“a”和“r”模式打开文件时,如果文件不存在,那么程序会报错。此时,对于“w”和“a”模式,有一个补救方法:采用“w+”和“a+”。这样在打开文件时,如果文件不存在,程序会创建一个以open中文件名命名的新文件,并进行后续操作,而不会报错(16)

我们引入两个简化编程的技巧——with开关文件和for循环文件。在例0.31中,用关键词with打开文件,并进行操作,with后续的子结构需要比with缩进一级。使用with打开文件时,如果打开方式有错误(例如,用“w”模式打开不存在的文件),则会整体跳过with结构;在with结构运行完毕后,Python会自动调用close()函数关闭文件描述符。在下例中,使用for循环读取fd内部的每一行,这相当于“for line in fd.readlines():”。

例0.31:使用with简化文件读写