12  选择程序结构

根据可计算理论,任何算法都可由三种基本结构组合而成:

  1. 顺序结构:按顺序依次执行语句。
  2. 选择结构:通过判断特定条件是否成立,决定是否执行语句。
  3. 循环结构:在一定条件下, 重复执行代码。

12.1 关系运算,逻辑运算,测试运算

关系运算用于比较对象的大小,测试运算比较对象的id是否相同,逻辑运算计算逻辑表达式的值。这三类运算的结果都是布尔值,可用于选择程序结构。

  1. 关系运算符。
  • >>=<<=:判断左操作数是否大于、大于等于、小于、小于等于右操作数。
  • ==:判断两个操作数是否相等。
  • !=:判断两个操作数是否不等。
注记注意
  • 比较两个浮点数是否不相等,不宜用==,而应该用math.close()
  • ==容易被误写为=
  1. 测试运算符。
  • isis not,测试操作数的id是否相等;
  • innot in,可以测试第1个操作数是否是第2个操作数的成员。第二个操作数是组合数据类型,如字符串,列表,元组,字典,集合等。
x = y = 3.145
print(x is y, x is not y)
x = 3.145
y = 3.145
print(x is y)
True False
False
print('机' in '我坐飞机会头晕')
print(1 not in [1, 2, 3])
True
False
  1. 布尔运算/逻辑运算。

布尔运算符将操作对象转换为布尔值。在一般的计算机教材中,布尔运算的规则如 图 12.1, 操作数是布尔值,返回的结果也是布尔值。

图 12.1: 计算机布尔运算规则。

但Python的布尔运算符则略有不同:

  • not返回布尔值;
  • andor进行短路运算(如果已经可以确定整个表达式的结果,则不再继续计算剩余的表达式),返回其最后计算的对象本身,不一定返回布尔值。
表 12.1: Python逻辑运算符
逻辑运算符 运算名称 说明 优先级
not 返回与操作对象相反的布尔值
and 若第1个操作数为假,返回之
否则返回第2个操作数
or 若第1个操作数为真,返回之
否则返回第2个操作数
print("芦花鸡" and "三黄鸡")
print("芦花鸡" or "三黄鸡")
print(not "芦花鸡")
三黄鸡
芦花鸡
False
表 12.2: 条件表达式的常用运算符,结合性默认从左至右,数字越小优先级越高。
运算符 说明 优先级 结合性
** 4 从右至左
+X
-X
~X
正号
负号
按位取反
5
*
@
/
//
%
乘法
矩阵乘法 (非原生支持)
除法
向下取整除法
求余数
6
+, - 加减法 7
in, not in
is, is not
<, <=, >, >=, !=, ==
成员检验
id检验
比较运算符
12
not 布尔非 13
and 布尔与 14
or 布尔或 15
x if e else y 条件表达式 16 从右至左
注记练习

计算下列表达式的值:

  1. not (7 <= 7)
  2. 8 > 5 or 6 < 4
  3. a < b and b < c or a > c,其中a = 5; b = 3; c = 7
  4. (x or y) and not (x and y),其中x = True; y = False
  5. 3 * 4 == 12 and 5 != 2
  6. (4 + 5) * 2 > 10 and 15 % 4 == 3
  7. not (3 ** 2 <= 10) or 7 // 2 > 3
注记记忆窍门

一些重要运算符的优先级的大概排序:

  • 算数>比较>逻辑
  • 逻辑运算符中,优先级降序排列为not, and, or,可以认为相应英语词的语气强烈程度递减。
注记练习

写出满足条件的表达式:

  1. 如果整数x能被4整除,输出True;
  2. 如果整数x不能被100整除,输出True。

12.2 if-else语句

if-else语句可以根据条件表达式的值,选择是否执行特定代码。

这对应生活中如果发生了...,就做...的情况。

# Python的if语句语法
if a:
    a为真时执行的操作
# 伪代码例子
if 天气预报有雨:
    不可在室外晾晒衣物

if 没听懂这个知识点:
    举手提问

if 好感度>=100:
    获得好结局
注记练习

编程完成下列任务:

  1. 如果一个整数x是否能被4整除。
  2. 判断一个整数x是否不能被100整除。
注记例:判断年份是否是闰年I

编程判断一个年份是否是闰年。 如果是闰年,输出类似于【相应年份是闰年】的提示语。 闰年条件:年份能被 4 整除但不能被 100 整除;或能被 400 整除。

year = 1900
if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0):
    print(f'{year}年是闰年。')
year = 2000
if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0):
    print(f'{year}年是闰年。')
2000年是闰年。

if语句流程图。

上例实现的是“仅在条件成立时做一些事,条件不成立时什么也不做”。 如果要“在条件成立时做一些事,在条件不成立时做另一些事”,可用if-else语句实现。

if a:
    a为真时执行的操作
else:
    a为假时执行的操作
注记例:判断年份是否是闰年II

接上例,要求不管年份是否闰年都要输出相应提示语。

year = 1900
if ((year % 4 == 0) and (year % 100 != 0)) or (year % 400 == 0):
    print(f'{year}年是闰年。')
else:
    print(f'{year}年不是闰年。')
1900年不是闰年。

if-else语句流程图。

上例包含1个条件判断,生出了两个分支; 存在包含多个条件,衍生出更多分支的复杂情况,可以使用嵌套的if-else语句。但Python编程实践中,else语句中含有的if情况被简写为elif,可以简化复杂的缩进,提升代码可读性。

注记理解if-else

英语中if表示“如果”,else表示“其他”,这里表示“(if判断的条件不成立时的)其他情况”,可以翻译为“否则”。

# elif写法
if a:
    a为真时执行的操作
elif b:
    a为假且b为真时执行的操作
else:
    a,b都为假时执行的操作

# 不使用elif而用if-else的写法
if a:
    a为真时执行的操作
else:
    if b:
        a为假且b为真时执行的操作
    else:
        a,b都为假时执行的操作
注记例:判断年份是否是闰年III

闰年条件:年份能被 4 整除但不能被 100 整除;或能被 400 整除。

year = 1900
if year % 400 == 0:
    print(f'{year}年是闰年。')
elif year % 100 == 0:
    print(f'{year}年不是闰年。')
elif year % 4 == 0:
    print(f'{year}年是闰年。')
else:
    print(f'{year}年不是闰年。')
1900年不是闰年。

闰年判断的if-else-elif语句流程图。
图 12.2: 闰年判断的if-else-elif语句决策树图,显示相同缩进的if,else,elif的条件构成一个对样本空间的划分,可以表现为从同一个树节点衍生出的多个分支。

根据具体场景的需求,if-else语句可以实现任意多条件与分支。

# 只有if, elif, 没有else
if a:
    a为真时执行的操作
elif b:
    a为假且b为真时执行的操作

# 多个elif
if a:
    a为真时执行的操作
elif b:
    a为假且b为真时执行的操作
elif c:
    a,b都为假且c为真时执行的操作
    ...
注记例:根据的分数出等级

成绩等级 = \begin{cases} 优 &mark\geqslant90\\ \text{良}&80\leqslant mark<90\\ \text{中}&70\leqslant mark<80\\ \text{及格}&60\leqslant mark<70\\ \text{不及格}& mark<60\\ \end{cases}

mark = 88
if(mark >= 90):
    grade = '优'
elif(mark >= 80):
    grade = '良'
elif(mark >= 70):
    grade = '中'
elif(mark >= 60):
    grade = '及格'
else:
    grade = '不及格'

print(f'{mark}分对应的等级是{grade}。')
88分对应的等级是良。

12.3 if-else嵌套

多个if-else语句嵌套,实现灵活的编程。 多个语句嵌套时,缩进表明ifelse的对应关系。

age = 25
income = 50000

if age >= 18:
    print('你是成年人。')
    if income >= 30000:
        print('你的收入达到了贷款标准。')
        if income >= 50000:
            print('你有资格申请高额贷款。')
        else:
            print('你有资格申请标准贷款。')
    else:
        print('抱歉,你的收入不满足贷款条件。')
else:
    print('抱歉,你还未成年,不能申请贷款。')
    if age >= 16:
        print('但你可以开始考虑未来的理财计划。')
你是成年人。
你的收入达到了贷款标准。
你有资格申请高额贷款。

上面代码对应的流程图。例子来源于C语言中文网
注记注意:if-else编程建议
  1. 当有多个条件时,注意各条件的排列顺序;
  2. 对于比较复杂的选择程序,建议验证各种可能的情况,否则程序中可能存在意想不到的错误。
  3. 理解if-else语句中的缩进水平揭示嵌套关系:
  • 缩进水平相同的ifelif相对应的条件是互斥的。
  • 缩进水平相同的ifelifelse(或者ifelse)各自对应的条件,构成对样本空间的划分/分割(partition),即这些条件互斥,且包含了所有可能发生的情况,无遗漏。可以画一个树图,将其范围内的语句块显示为从同一个节点衍生的分支( 图 12.2 )。
  • 设有两个if-else语句的缩进水平不同,b比a缩进得更多。缩进水平更多的b,嵌套于缩进水平更少的a,在流程图中表现为b在a衍生出的分支中出现。
if a:
    语句块1
    if b:
        语句块2
    else:
        语句块3
else:
    语句块4

上述代码对应的流程图。

12.4 更多例子

  1. 输入两个整数,将其按从大到小的顺序输出。
a = int(input("请输入第1个整数"))
b = int(input("请输入第2个整数"))
print(str.format("输入值:{0},{1}", a, b))
if(a<b):
    a, b = b, a
print(str.format("降序值:{0},{1}", a, b))
注记练习:整数排序II
  1. 输入3个整数,降序输出它们的值。
  2. 输入3个整数,升序输出它们的值。
  1. 输入一个实数,输出符号函数值:如果输入正数,输出1;如果输入负数,输出-1;如果输入0,输出0。
number = 10
if number > 0:
    sign = 1
elif number < 0:
    sign = -1
else: 
    sign = 0

print(sign)
1
注记练习

输入一个点的x,y坐标,输出其在坐标系中的位置:

  1. 第一象限:x, y > 0
  2. 第二象限:x < 0, y > 0
  3. 第三象限:x, y <0
  4. 第四象限:x > 0, y < 0
  5. 其他情况,包括原点(0,0),y轴上(除去原点),x轴上(除去原点)。

象限示意图
  1. 求解一元二次方程II。输入三个数 a,b,c,求解方程 ax^2 + bx + c = 0。分情况讨论结果:
  • a = 0 ,解一元方程
  • a\neq 0 ,解一元二次方程:
    • \Delta = b^2-4ac>0 ,有两个不等实根
    • \Delta = 0 时,有两个相等实根
    • \Delta <0 时,输出“无实根”,或输出2个共轭复根
import math

# 输入三个系数
a = float(input('请输入系数 a: '))
b = float(input('请输入系数 b: '))
c = float(input('请输入系数 c: '))

if a == 0:
    # 一元一次方程
    print('不是二次方程。')
    if b == 0:
        if c == 0:
            print('有无穷多解。')
        else:
            print('无解。')
    else:
        x = -c / b
        print(f'解为x = {x}')
else:
    # 一元二次方程
    delta = b**2 - 4*a*c
    
    if delta > 0:
        x1 = (-b + math.sqrt(delta)) / (2*a)
        x2 = (-b - math.sqrt(delta)) / (2*a)
        print(f'有两个不等实根:x1 = {x1}, x2 = {x2}')
    else:
        if delta == 0:
            x = -b / (2*a)
            print(f'有两个相等实根x1 = x2 = {x}')
        else:
            # delta < 0
            print('无实根')

12.5 if-else运算符

表 12.2 中有if-else运算符。 此运算符与前述的区别在于,程序会利用含有if-else运算符的if-else表达式的值。 而if-else语句中,冒号后面要通常是有副作用的完整的语句,而不仅仅是一个返回值的表达式。

# if-else 运算符语法
x if e else y
e为真时表达式的值 if e else e为假时表达式的值
# if-else运算符的正确用法
x = '应该输出这个' if 1+1 ==2 else '不该输出这个'
print(x)
应该输出这个
# if-else运算符的错误用法
x = print('应该输出这个') if 1+1 ==2 else print('不该输出这个')
print(x) # x值是None
应该输出这个
None

if-else运算符的结合性是从右至左,而相关优先级下只有这一个运算符,说明此结合性只有在x if e1 else y if e2 else z这样的语句中发挥作用:

  • 先计算y if e2 else z的值(假设值为result
  • 然后再计算y if e2 else result

12.6 练习

  1. BMI指数计算II。

BMI指数可衡量人的健康情况,计算公式为 BMI=体重(kg)/身高^2(m^2)。计算自己的BMI指数,并根据 图 12.3 ,输出在中国参考标准下对应的BMI分类。

图 12.3: BMI指数标准

BMI指标不适合下列人群:

  • 未满18岁;
  • 运动员;
  • 正在做重量训练;
  • 怀孕或哺乳中;
  • 做过手术,身上有其他材料的患者;
  • 身体虚弱或久坐不动的老人。
  1. 编程,让用户答一道客观题,根据用户输入,输出提示语。 如题目为1+1,如果玩家的输入等于2,返回“回答正确,你真棒!”,否则返回“答错了,再想想。”

  2. 狗的年龄换算:狗生的头两年,每1岁相当于人类的10.5岁;之后狗生的每一年,相当于人类的4年。编写程序,输入狗的年龄,输出其相当于人类的年龄。

  3. 输入三个浮点数,判断其是否能够成三角形的三边(任意两边之和大于第三边),输出判断结果。

  4. 视觉小说I。(这个系列为基于Python的视觉小说开发引擎Ren’Py的学习者准备)

视觉小说的历史可追溯到20世纪末的文字冒险程序,如《巨洞冒险》:游戏场景通过文字表现,玩家通过输入简单的字符串与程序互动。

根据你的兴趣,选作下列题目:

  • 恋爱模拟:游戏中“你”的男朋友/女朋友会问“你”几个问题,根据作答结果,你将迎来“好结局”或“坏结局”。
  • 抓怪兽模拟:设定怪兽的生命值,“你”有3次行动机会,每次行动可以选择攻击或抓捕。当怪兽的生命值低于一定阈值时,抓捕成功,否则抓捕失败。为了增加可玩性,攻击造成的伤害可以是一定范围内的随机数(可用random模块实现);在更复杂情形下,抓捕成功概率是一个与生命值残值有关的函数(实现可能需要numpy等第三方库)。
  • 其他适合用文字呈现的游戏。

最好将代码保存为Python脚本,通过运行脚本的方式启动游戏。 以免源代码直接暴露给玩家,失去趣味性。

12.7 参考资料

Jackwoon,知乎:身体质量指数BMI