本节书摘来自华章计算机《从问题到程序:用Python学编程和计算》一书中的第2章,第2.11节,作者:裘宗燕 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.11 补充材料
本书各章的主要内容将围绕着怎样通过编程解决计算问题展开,正文中对Python语言的机制只做必要的说明,有些细节情况没有涉及。另外,用Python编程也有许多有趣而且有用的技术。如果在各章的主要部分详细罗列,也可能冲淡讨论的主线。但是上述两方面的一些情况也值得介绍。本书采用的方法是在一些章的最后增加称为“补充材料”一节,补充一些细节,供读者参考,也供用本书教授课程的教师选用。
除了讨论语言细节和编程技术的两个小节外,有时还总结了一些常用的编程模式。练习中的第1题总是对本章内容做些总结,其中列出本章讨论过的重要概念。
2.11.1 语言细节
这里介绍Python语言中的一些与本章内容有关的情况,作为本章内容的补充。这里介绍的情况在本书中并不使用,对于初学者并不重要,可以有选择地阅读。提供这部分内容有两方面意义,首先是使本书中对Python语言的介绍较为完整。另一方面,读者在进一步使用Python去解决更复杂的问题时,也可能需要用到这里介绍的功能。
整数的二进制、八进制和十六进制字面量形式
Python只有一种整数,即是类型为int的整数。但另一方面,这个语言却为整数提供了多种不同的字面量描述方式。前面介绍的十进制字面量形式使用最普遍,但Python还提供了二进制、八进制和十六进制的整数字面量形式。现在介绍有关情况。
首先,二进制、八进制和十六进制的整数字面量形式,都由一个引导部分和一个数值部分组成,一个整数仍然写成一个连续的字符序列。这三种字面量的引导部分都以数字0开头,在0之后用一个字符相互区分,而后紧接着是一串表示其数值的数字(对于十六进制,其中还能包含一些英文字母,见下)。
二进制整数字面量的引导部分是0b或者0B,随后表示数值的部分中只能用两个数字0和1,是一段0/1序列。例如0b110101和0B110100。在b或B之后立刻出现几个0也允许,不影响结果的数值。二进制数的数值按如下方式计算:
这里实际上假设写出的二进制字面量包含n + 1位二进制数字。很容易看到,Python的二进制字面量只是整数的一种写法,例如:
>>> 0b110101
53
>>> 0B01001110
78
>>>
在二进制字面量里出现超出0和1的数字将作为出错。此外,Python标准函数bin返回与其整数参数对应的二进制字面量形式(结果是一个字符串)。例如:
>>> bin(78)
'0b1001110'
八进制整数字面量的引导部分用0o或者0O(这两种形式都很糟糕,0和O太像了),随后可以写出任意长的数字0到7的序列。其计值公式是:
例如:
>>> 0O12
10
>>> 0O1234567012345670
45954944846776
与二进制类似,表示数字的部分中不能出现数字8或9。标准函数oct返回与其整数参数对应的八进制字面量形式(是一个字符串)。
十六进制整数字面量的引导部分用0x或者0X。这里有一点麻烦:表示十六进制数的数值需要16个数字,但阿拉伯数字只有10个。Python采用计算机领域的习惯做法,用最前面6个英文字母填补空缺:a或A表示10,b或B表示11,c或C表示12,d或D表示13,e或E表示14,f或F表示15。下面是两个十六进制字面量:
>>> 0X12
18
>>> 0xdeadbeeffeeddeaf
16045690985374408367
十六进制数表示的计值公式与上面类似:
![1286256acf078da99bb6b32cff7abdaac29dc0b9](https://i-blog.csdnimg.cn/blog_migrate/6c6bf715831d24ec6e1b8368fa3e3853.png)
标准函数hex返回其整数参数对应的十六进制字面量字符串。
用二进制、八进制、十六进制字面量都可以描述任意大的整数。
整数的位运算
计算机里的数据都用二进制编码的形式表示,位(即二进制位,bit)是最基本的编码单位,也是最小的数据表示单位。在实际问题中,有些对象的变化情况很简单,只用一个或几个位就能表示,如果程序里这种数据很多,把它们表示为某类型的对象,可能造成很大的存储浪费。一种可能办法是把多个这类数据对象存入一个整数类型的对象。此外,一些与底层有关的程序可能需要操作二进制位数据。例如,硬件工作状态信息通常用二进制位串表示,操作硬件时就需要用位串形式发命令。为适应这些情况,Python语言提供了针对整数中二进制位的操作。有关的运算符称为按位运算符。
先介绍基本的位运算,它们是按位运算符的基础。一个二进制位只能取值0或1,位运算就是对二进制位的运算,从一个或两个0/1值出发,算出0/1结果。常用位运算共有四个,其中“否定”为一元运算,其他都是二元运算,运算规则很简单,见下表:
从这个表可以看出,否定就是1变0而0变1;只有1和1“与”的结果是1,其他情况的结果均为0;0和0“或”的结果为0,其他情况结果都是1;“异或”运算看两个被操作位是否相同,相同时是1,否则是0。
与上面的位运算相对应,Python语言定义了四个按位运算符,把它们作用于整型对象,得到整型结果。这些按位运算符把整数看成二进制位的序列。按位否定运算符是一元运算符,作用于一个整数类型的运算对象,其余三个都是二元运算符,作用于两个整数类型的运算对象。在运算时,它们对运算对象的一个个(或一对对)二进制位分别做位运算,得到结果的各位。四个按位运算符是:
![d0576cc37163d8dfa2186c6cf8a7f495dbfdd5ff](https://i-blog.csdnimg.cn/blog_migrate/28b4669a6b0273a95d81fcd7564e8935.png)
看一个例子,假设变量x和y都是16位整数,它们值分别是:
x: 0010,1001,0101,0111
y: 1001,1100,1111,1010
对x和y做各种按位运算,得到的结果如下:
~x 1101,0110,1010,1000
x & y 0000,1000,0101,0010
x | y 1011,1101,1111,1111
x ^ y 1011,0101,1010,1101
请读者根据这个例子,对照上面说明,设法弄清各按位运算符的意义。注意,这里整数表示中的逗号只是为了阅读方便。
这些运算符可以作用于任意大的整数对象,得到任意大的结果。如果两个整数的长度不同,Python有默认的处理方式,总之不会丢失信息。
除了上面4个按位运算符外,Python还有两个与二进制位有关的整数运算符,分别是左移运算符“<<”和右移运算符“>>”。它们的左边是被位移的整数对象,右边的整数表示希望左移或右移的位数。举例说,对上面的y将有:
y << 3: 1110,0111,1101,0000
y >> 3: 0001,0011,1001,1111
可以看到,左移(或右移)空出的二进制位全部补0。
另请注意,上面的这六个运算符都是做运算,产生新的整数对象。例如3 << 3“算出”一个结果(一个新的整数对象24)。假设变量z的值是3,z << 3得到24,但z的值不变。要改变z的值就需要赋值,或者用下面介绍的扩充赋值运算符。
上述运算符的优先级情况有些复杂:一元否定运算符的优先级与一元正负号相同,移位运算符的优先级低于整数加减,三个二元按位运算符的优先级互不相同,从高到低依次为 &、|、^,但都低于移位运算符。所有二元运算符都按从左到右结合。例如x << 3 >> 5相当于(x << 3) >> 5。由于优先级的情况比较复杂,建议少写过分复杂的表达式,多用括号显式描述运算的顺序。
与上述5个二元位运算符相对应,Python有5个扩充赋值运算符,它们分别基于赋值符之后的第二个参数修改左边表达式的对象(最简单情况用变量表示):
![4a04fa669166924f154902369e272f3ac8947a46](https://i-blog.csdnimg.cn/blog_migrate/2e3c4113f2e5c5fc59eafd3ce0ecab24.png)
例如,x <<= 5效果相当于x = x << 5,但书写方便,通常效率更高。
有关浮点数的进一步说明
高级语言的浮点数一般都直接映射到语言系统运行所在的计算机硬件,因此高级语言中的浮点数及浮点数计算直接反映了硬件的相关特征。在计算机设计层面,目前大多数硬件的浮点数计算都采用IEEE 754标准。这里的IEEE是电气和电子工程师协会的简写,它是目前全球最大的一个非营利性专业技术学会。IEEE 754是IEEE颁布的一个浮点数算术系统标准,被大多数计算机硬件厂商采纳。Python语言并没有强制性地规定采用IEEE 754浮点数(如果那样,在不执行这个标准的硬件上将很难实现Python),但常见环境中运行的Python中的浮点数应该符合这个标准。下面介绍最常用的浮点数情况。
在常规系统上运行的CPython都采用IEEE 754的双精度浮点数标准作为语言的浮点数。这种浮点数用64位二进制码表示,其中指数部分用11位,可以表示-1023到1023;表示数值的部分(术语是尾数部分)用52位,另有一个符号位表示正负数。
指数部分的大小决定了浮点数的表示范围,绝对值最大(最小)大约是±21023的量级。换算到十进制数,表示的范围(如前面所说)大致为±5×10-324~1.7×10308。尾数的位数决定了浮点数的表示精度,52位二进制数大约相当于十进制的16到17位。超过上述范围的实数在这里无法表示。即使在范围内,浮点数表示也受到精度的限制,只能表示数轴上一个个能用二进制编码形式表示的孤立点。
IEEE 754的具体表示方式还有些细节,但知道上面这些对于初学者已经够了。从基本编程的需要看,只需了解这种浮点数标准的表示范围和精度。许多其他语言的实现也采用IEEE 754的双精度浮点,其他Python实现多半也采用这个标准。
浮点数舍入转换
从浮点数转换到整数,默认转换方式是舍去小数部分,通常称为截尾。内置函数round采用另一种转换方式,通过舍入得到与浮点数最近的整数。但什么是最近呢?中小学的算术里教过“四舍五入”的舍入规则,但显然这一规则偏向于入,从统计的观点看,这样得到的整数值偏大。银行总按四舍五入付钱就会亏本,长期做下去累积的“入”也会导致亏本很多。为了防止这种情况,人们提出了另一种更为公平的舍入方式。
Python的round函数采用所谓“银行家舍入”方法,可称为“四舍六入五取偶”舍入,这也是目前常见计算机硬件采用的舍入计算标准(IEEE 754浮点数标准中的舍入计算标准方法,目前编程语言和工具大都直接借用这一舍入计算标准)。
具体说,如果需要舍入部分的最高位小于等于4和大于等于6,就直接分别舍去或者进位。假设需要舍入的那段数字的最高位是5,如果在这个5之后还有不为0的有效位(也就是说,明确地大于5),转换时就进位。如果5之后都是0位,则根据5的前一位舍入:前一位是奇数时进位,前一位是偶数时舍去。这里把0也看作偶数。与简单的四舍五入,“银行家舍入”在概率上更公平。按照这种规则,可以看到:
>>> round(0.5)
0
>>> round(1.5)
2
>>> round(-0.5)
0
>>> round(-1.5)
-2
字符串和换意序列
在写字符串时有些字符无法直接写出,换行符是这种字符的典型代表。由于有这种情况,Python(与其他一些语言一样)引入了换意序列的概念,用一种特殊形式的字符序列(包含两个或更多字符)表示字符串里的一个字符。换意序列的第一个字符总是下划线符,也就是说,字符串里出现的下划线符总表示换意序列开始,后面字符(序列)决定换意序列表示的字符。前面介绍过几个常用换意字符,下面是它们和另外几个及其解释:
其中,“换行符”表示反斜线后紧跟换行。“ooo”表示反斜线后紧跟3位八进制数字,这种形式的换意序列表示编码为八进制为ooo的字符。“xhh”表示反斜线后有一个x,后跟2位十六进制数字(包括A/B/C/D/E/F),这种换意序列表示编码为hh的字符。
除上面这些表示形式外,Python还支持用于写出所有Unicode字符的换意序列形式。这方面的细节这里不进一步介绍了。
用一对三个引号的形式,在字符串字面量中可以包含换行,不必再写换意序列“n”。此外,采用三个引号的形式,多数时候也不需要用单引号和双引号的换意序列“'”和“"”。但有时还可能需要,例如下面字面量没问题:
""""Is this
the book
of you?", she asked"""
开头的连续四个引号被正确解析为一个连续三引号和一个双引号。但
""""Is this
the book
of you?""""
将报错。开始的连续四个双引号可以正确解析,但最后的四个双引号不行:解释器看到前三个双引号,认为它是字符串的结束标志,字符串到此结束。又看到一个双引号,解释器认为另一个字符串从这里开始。遇到换行,它认为字符串没完,报语法错误。要正确写出这个字符串(内容是一对双引号括起的一句话),可以写:
""""Is this
the book
of you?\""""
或者更简单的,用一对三个单引号作为字符串括号。
如果在一行里连续写出几个字符串,解释器会自动将其连接成一个字符串:
>>> s = 'abc' "123"
>>> s
'abc123'
但是,解释器只是在处理字符串字面量,才这样做。例如:
>>> t = '456'
>>> s t
SyntaxError: invalid syntax
如果要拼接作为变量值的字符串,必须用拼接运算符(加法符号)。
基本语句
表达式可以写在程序中任何应该写语句的位置,这样的表达式就构成了一个表达式语句。在一般程序里,把普通表达式(例如算术表达式1 + 2等)作为语句使用意义不大。运行中执行这个语句时就求值该表达式,求出值后该语句的执行完成,表达式的值随后将被丢掉,不产生任何效果(除了表达式计算可能耗费计算机时间)。
有用的表达式语句主要是函数调用。在Python里,函数调用是一类基本表达式。独立写出的函数调用就构成了一个表达式语句。前面程序里已经多次出现这种表达式语句,例如对内置函数print的调用语句。实际上,任何无特定返回值的函数(实际上返回None),通常都以表达式语句的方式使用。
Python允许在一行中写多个语句,这时要求语句之间加分号。例如:
x = len; y = x + 1
这样一行仍看作一个语句,在其执行时将顺序执行其中的成分语句。最后的成分语句执行完毕时整个语句的执行完成。这种语句是顺序语句的一种形式。当然,这种形式中也可以写任何语句,不仅是赋值语句。例如在一个循环里写:
x = y + 4; continue
虽然语言允许上面的形式,有时也会看到有人把几个简单语句写在一行。但在Python编程实践中,人们不大倡导这种形式。在绝大多数Python程序里,人们坚持一行一个语句的基本规则。在一些情况下用并行赋值语句同时给几个变量赋值。
2.11.2 编程技术
本节讨论几个与编程有关的技术问题。
条件语句与条件表达式
在一些情况下,条件语句和条件表达式都能使用。例如下面同样函数的两个定义:
def abs(x):
if x < 0:
return -x
else:
return x
def abs(x):
return -x if x < 0 else x
两个函数功能完全一样,但后一个简单许多。参考这个实例,可以总结出适合使用条件表达式的情况:在需要根据条件,从两种不同的表达式计算中选一个,而且计算比较简单时,采用条件表达式可以简化程序。条件语句适合用于各种赋值情况,其一个分支中可以包含任意复杂的操作序列。用来处理上面问题,是大材小用了。
在后面章节里,还会看到许多使用条件表达式的有意思的例子。
写表达式的技术
如果需要写的计算表达式非常复杂,应该设法做出安排,使写出的程序代码清晰易读,容易检查表达式书写的正确性。有两种方法可以参考。
其一是分解表达式,用一个或几个中间变量记录表达式中子部分的结果,而后用这些变量的值组合出最终的表达式。适当的分解有助于保证表达式的可读性和正确性。
如果确实需要或者希望写很长的表达式,那就需要安排好表达式的多行格式。首先做好准备,为能把表达式写在多个行里,可以参考前面基于三边求三角形函数中条件的写法,先加入一个括号保证解释器不会把表达式截断,而后在每行适当的地方断行,各行中属于同层的结构相互对齐(仿照Python语言的格式)。例如,下面是一个长表达式:
x = ((a1 * a2 - b1 * b2 - c1 * c2 - d1 * d2) +
(a1 * b2 + a2 * b1 + c1 * d2 - c2 * d1) * i +
(a1 * c2 + a2 * c1 + b2 * d1 - b1 * d2) * j +
(a1 * d2 + a2 * d1 + b1 * c2 - b2 * c1) * k)
为能写跨越多行的长表达式,在上面表达式开始加了一个括号。在上述表达式中,我们把属于同一层的子表达式对齐,分别放在几行。这样写表达式,很容易检查,容易发现错误。IDLE或其他支持Python的开发系统都能帮助维持良好的表达式形式。但如何断行等,还是需要人做好安排。
数值计算函数中错误情况的返回值
有些数值计算函数不是全函数(相对于参数的类型而言),如果被调用时得到的参数不满足需要,它将无法返回一个正确的结果。在这种情况下可以考虑报错(后面介绍),也可以考虑让函数返回一个特殊值。不同的处理方式各有优缺点。
本章中出现了几种处理方法:求阶乘函数对于负的参数都返回0;计算三角形面积的函数,对于不能构成三角形三边的参数,返回float("nan")表达式的值。后面还会看到一些情况,这些做法都可以参考。
本章介绍的Python关键字
这里列出本章讨论过的Python关键字,供读者参考:from,import,and,or,not,True,False,if,else,elif,for,in,while,break,continue,def,return,None,pass。在总共33个关键字中,本章已经介绍了19个。包括:
- 3个特殊字面量,表示逻辑常量的True和False,以及特殊值None;
- 3个逻辑运算符and、or和not(not还有其他使用方式,见后面的介绍);
- 流程控制结构中用6个,if,else,elif、for、in、while;
- 专门语句用4个:break,continue,return,pass;
- 程序包导入语句用from和import;
- 函数定义def。