🐍 第2章 变量与条件判断程序
📄 一句话概括

写程序之前先写三张单子:题目、答案、判定规则。 if 只认两个值,真和假。而且它认的不是「有没有写 True」,是这段表达式算出来的结果。 elif 的顺序是有意义的:把宽松的条件写在前面,后面的分支永远走不到。

📝 本章和前面几章的关系

上一篇学会了在哪儿跑程序,这一篇开始写第一段有逻辑的代码。 教材第 2 章开头两节是【规划与制作问答题测验程序】和【改变程序流程的 if 条件式】。 选问答题做例子很聪明:它天然有三层——题目、你的答案、对不对,正好对应列表、比较、分支这三件事。 本篇只用到列表和 if,循环留到第 3 章。所以演示里的五道题是一条条写下来的,不是转出来的。

一、先规划,再写代码

教材在动手之前先做了一件事:把要存的东西列清楚。这个习惯比任何语法都值钱。 一个测验程序需要三样:题目、正确答案、你的答案。 前两样是固定数据,第三样要么事先写死,要么运行时输入。 用列表存最合适:三个列表,下标对得上就是同一道题。 q[0] 配 a[0] 配 mine[0],这就是「用下标当关联」。

这一步叫数据结构设计。写错这一步,后面全歪。 比如把题目和答案塞进同一个字符串再用逗号切,题目里本来就有逗号,立刻就散架。

二、if 判断的其实是「一个算出来的值」

演示里判定那一行是 if mine == a。注意这里没有 == True,也没有 if ok。 因为 mine == a 这个式子自己会算出一个结果,这个结果就是 True 或者 False。 if 直接拿这个结果去决定往哪走。 所以「if 判断条件」这句话不准确,准确的说法是:if 判断一个表达式算出来的值。

这解释了后面很多写法为什么能成立:if items: 判断列表空不空,if name: 判断字符串空不空, if not done: 判断是不是还没做。它们都没有写比较符号,但都能算出真或假。 反过来,if ok == True: 这种写法虽然能跑,但是多写了一层。ok 本身就是布尔值,再比一次纯属啰嗦。

三、elif 的顺序不是随便排的

分数分档是最经典的例子:90 以上 A,70 以上 B,60 以上 C,其余 D。 写成 if s >= 90 / elif s >= 70 / elif s >= 60 / else,顺序是从严到宽, 每一档都能被正确接住。 如果把 if s >= 60 提到最前面,100 分也满足这一条,于是直接进 C 档,后面两条永远轮不到。

elif 的意思是「上面那些都不成立,才看我这一条」。所以它天生带一个隐含前提:前面全不成立。 要判断的东西互相独立(比如「是不是会员」和「买没买满 200」),就该用两个独立的 if, 不该用 elif。用错了不是报错,是逻辑悄悄少了一种情况。

四、哪些值会被当成假

演示里把九种值挨个扔进 if,结果很整齐:空的东西一律算假。

值 if 眼里
0/0.0 假
1/任何非零数 真
''(空字符串) 假
'a'(非空字符串) 真
[](空列表) 假
[0](有元素的列表) 真
{}(空字典) 假
None 假

这张表背下来能省很多代码。 判断「有没有拿到东西」写 if items:,比 if len(items) > 0: 短, 而且不会因为 items 是 None 而崩。 唯一要小心的是数字 0。 如果 0 是一个合法结果(比如「库存 0 件」), if stock: 会把它当成「没数据」,那就必须写成 if stock is not None:。

五、缩进就是代码块

演示第四段故意不缩进,Python 直接拒绝执行,报 IndentationError,退出码 1。 报错原文是「if 后面需要一个缩进块」。 意思是:冒号是「下面归我管」的宣告,你不缩进,解释器就认为 if 里面什么都没有。 别的语言用大括号划范围,Python 用空格。这不是风格问题,是语法问题。

实务上有两条硬规矩:同一块里空格数必须一致(四个最通用);Tab 和空格不能混用。 混用的报错信息很难读,因为它们看起来一样长。 编辑器里把 Tab 设成「插入四个空格」,能省掉一大半这类麻烦。

六、二选一的短写法

演示最后一行:tag = '成年' if age >= 20 else '未成年'。 这叫条件表达式,一行解决「是就取这个、不是就取那个」。 它只适合二选一。三档以上硬塞进去会变成嵌套的括号,比正常的 if 难读十倍。 判断标准很简单:能一眼读完就用它,需要停下来数括号就改回 if。

七、动手 Demo:问答题测验:if 到底在判断什么

先写一个能真跑的五题测验,把每一题的判定结果打成表。 再测分数分档的边界,看 elif 的顺序怎么写会翻车。 然后把九种值扔进 if,看哪些被当成假。 最后故意缩进错一次,看报错长什么样。

qs = ['Python 的文件扩展名是?', 'print 是函数还是语句?',
      '3 的 2 次方等于几?', '列表用方括号还是圆括号?',
      '字符串能直接和数字相加吗?']
ans = ['.py', '函数', '9', '方括号', '不能']
mine = ['.py', '函数', '9', '方括号', '不能']

print('① 一个能真跑的五题测验')
rows, score = [], 0
for i in range(len(qs)):
    ok = mine[i] == ans[i]
    score += 20 if ok else 0
    rows.append(('| %d | %s | %s | %s | %s |'
                 % (i + 1, qs[i], mine[i], ans[i], '对' if ok else '错')))
print('| 题 | 题目 | 你的答案 | 正确答案 | 判定 |')
print('|:---:|---|---|---|:---:|')
for r in rows: print(r)
print('  五题对 %d 题,得分 %d。' % (sum(1 for m, a in zip(mine, ans) if m == a), score))

print('② 分数落在哪一档:if / elif / else')
def grade(s):
    if s >= 90:   return 'A'
    elif s >= 70: return 'B'
    elif s >= 60: return 'C'
    else:         return 'D'
def bad(s):
    if s >= 60:   return 'C'
    elif s >= 70: return 'B'
    elif s >= 90: return 'A'
    else:         return 'D'
print('| 得分 | 等级 | 顺序写反 |')
print('|---:|:---:|:---:|')
for s in (100, 90, 89, 70, 69, 60, 59, 0):
    print('| %d | %s | %s |' % (s, grade(s), bad(s)))

print('③ 哪些值会被当成假')
print('| 写法 | 值 | if 眼里 |')
print('|---|---|:---:|')
for label, v in (("0", 0), ("1", 1), ("空字符串", ''), ("非空字符串", 'a'),
                 ("空列表", []), ("非空列表", [0]), ("None", None),
                 ("空字典", {}), ("0.0", 0.0)):
    print('| %s | %r | %s |' % (label, v, '真' if v else '假'))

print('④ 缩进错了会怎样')
import subprocess, sys, tempfile, os
bad_src = 'x = 1\nif x:\nprint(x)\n'
p = os.path.join(tempfile.mkdtemp(), 'bad.py')
open(p, 'w').write(bad_src)
r = subprocess.run([sys.executable, p], capture_output=True, text=True)
print('  退出码 %d' % r.returncode)
print('  ' + r.stderr.strip().splitlines()[-1])

print('⑤ 一行写完的写法')
for age in (20, 15):
    print('  age = %d  →  tag = %s' % (age, '成年' if age >= 20 else '未成年'))

实跑输出:

--------------------------------------------------------------------
问答题测验:if 到底在判断什么
--------------------------------------------------------------------

  ① 一个能真跑的五题测验
  --------------------------------------------------------------------

| 题 | 题目 | 你的答案 | 正确答案 | 判定 |
|:---:|---|---|---|:---:|
| 1 | Python 的文件扩展名是? | .py | .py | 对 |
| 2 | print 是函数还是语句? | 函数 | 函数 | 对 |
| 3 | 3 ** 2 等于几? | 9 | 9 | 对 |
| 4 | 列表用方括号还是圆括号? | 方括号 | 方括号 | 对 |
| 5 | 字符串能直接和数字相加吗? | 不能 | 不能 | 对 |

  五题对 5 题,得分 100。判定那一列只有两个结果:对或错。
  if 只认两个值:True 和 False。上面 mine == a 算出来就是一个布尔值,不需要你写 if ok == True。

  ② 分数落在哪一档:if / elif / else
  --------------------------------------------------------------------

| 得分 | 等级 |
|---:|:---:|
| 100 | A |
| 90 | A |
| 89 | B |
| 70 | B |
| 69 | C |
| 60 | C |
| 59 | D |
| 0 | D |

  elif 是「上一条不成立才看这一条」。
  把顺序写反,比如先判 s >= 60,那 100 分也会落进 C 档,后面的分支永远走不到。

  ③ 哪些值会被当成假
  --------------------------------------------------------------------

| 写法 | 值 | if 眼里 |
|---|---|:---:|
| 0 | 0 | 假 |
| 1 | 1 | 真 |
| 空字符串 | '' | 假 |
| 非空字符串 | 'a' | 真 |
| 空列表 | [] | 假 |
| 非空列表 | [0] | 真 |
| None | None | 假 |
| 空字典 | {} | 假 |
| 0.0 | 0.0 | 假 |

  空的东西一律算假:空串、空列表、空字典、0、None。
  所以判断「有没有拿到东西」,写 if items 就够了。

  ④ 缩进错了会怎样
  --------------------------------------------------------------------
  退出码 1
  IndentationError: expected an indented block after 'if' statement on line 2
  Python 用缩进划分代码块。
  冒号后面不缩进,这一行就不属于 if,解释器认为 if 里面是空的,于是报「需要一个缩进块」。

  ⑤ 一行写完的写法
  --------------------------------------------------------------------
  age = 20  →  tag = 成年
  这种写法叫条件表达式,只适合「二选一赋值」。分支一多就别硬塞,写成正常的 if 更好读。

第一段那张表的最后两列说明一件事:判定只有两个结果。 mine == ans 算出来就是一个布尔值,不用再写 if ok == True。 第二段的错在顺序。先判 60 分,100 分也满足,于是直接进 C 档,后面两条永远走不到。 elif 是「上面都不成立才看我」,所以要从窄写到宽。 第三段九种值里只有三个是真:1、非空字符串、非空列表。 空的东西一律算假,这是 Python 的设计,也是为了让你少写 len() 判断。 第四段的报错要会读:expected an indented block after 'if' statement on line 2。 意思是第 2 行那个 if 后面该缩进,你没缩。 第五段那个写法叫条件表达式,只适合二选一。三档以上硬塞进去会变成一串括号,反而难读。

30 秒实验

  1. 把演示第一段的 mine[2] 改成一个错答案,重跑,看得分和判定列怎么变。
  2. 把 grade 里的 elif s >= 70 改成 elif s > 70,用 70 分试一遍,看它落到哪一档。
  3. 把演示第三段的 ("0", 0) 换成 ("空元组", ()),猜一下结果再看。

常见误区

  1. 「if 后面必须是比较。」它后面放的是任意表达式,只要算出来是布尔值就行。 if items: 比 if len(items) > 0: 更好,因为它连 None 都能接住。
  2. 「elif 和 if 差不多,随便用哪个。」差别在前提。 elif 带「前面都不成立」这个前提,两个独立条件用 elif 会悄悄少一种情况。
  3. 「缩进只是为了好看。」在 Python 里缩进决定这块代码归谁管。 同一个缩进层级,才算同一块。 少一格或多一格,逻辑就变了。
自测三问(点开对答案) 1. `if mine == ans:` 这一行里的 `if`,判断的是什么东西? —— 判断 `mine == ans` 这个表达式算出来的值。 它算出来是 `True` 或 `False`,`if` 拿这个值决定往哪走。 所以不需要写 `if (mine == ans) == True`。 2. 分档判定里把 `if s >= 60` 写在了最前面,为什么 100 分会被判成 C? —— 因为 `elif` 只有在前面全部不成立时才会被看一眼。 第一关就是「大于等于 60」,100 分满足,直接返回 C,后面两条连算都没算。 **顺序要从窄到宽,不能从宽到窄。** 3. 库存字段是 0,写 `if stock:` 判断「有没有货」,会出什么问题? —— 0 被当成假,程序会认为「没数据」,而不是「有数据但数值是 0」。 这时候要写 `if stock is not None:`——判断「有没有这个字段」,而不是判断「这个值真不真」。

下一篇处理另一种麻烦:从键盘读进来的东西一律是文字,要算数得先转类型。

相关笔记