CS1602计算导论
第 4 讲Part 1 计算基础与 Python 入门AI Level 0

条件、循环与函数

Condition, Loop and Function

到目前为止你的程序都是从上到下一条道跑到黑。这一讲给它装上两样东西——分岔和重复。有了它们,Python 就能表达任何可计算的过程。

本讲结束后你应当能

  • 用 if / elif / else 表达多路分支,并避开缩进陷阱
  • 判断该用 for 还是 while,并正确使用 break 与 continue
  • 说清楚 for-else 里的 else 什么时候执行
  • 用默认参数、关键字参数和 *args / **kwargs 写出灵活的函数
  • 给函数加类型提示,并说出它为什么值得写
本页目录

从直线到分岔

前三讲你写的程序都是一条直线:第一行、第二行、第三行,跑完结束。这样的程序做不了什么——它对输入毫无反应,每次运行都干同样的事。

这一讲加两样东西:

  • 分岔(条件):根据情况走不同的路
  • 重复(循环):把一件事做很多遍

听起来简单,但有了顺序、分岔、重复这三样,就足以表达任何可计算的过程。这不是修辞,是可以证明的结论(结构化程序定理)。你已经拥有了一门图灵完备语言的全部控制结构。

条件

布尔表达式

L2 讲过,比较的结果是 True 或 False:

print(3 > 2)
print(3 == 2)
print("abc" != "abd")
print(3 >= 3)

用 and、or、not 组合。注意运算顺序:not 最紧,然后 and,最后 or。

print(True or False and False)     # and 先算 → True or False → True
print((True or False) and False)   # 加括号改变顺序 → False

链式比较

Python 允许把比较串起来,写法和数学一样:

x = 5
print(1 < x < 10)          # 等价于 1 < x and x < 10
print(0 <= x <= 100)

这是 Python 相对 C、Java 的一个优点——那些语言里 1 < x < 10 会先算 1 < x 得到 True,再把 True 和 10 比较,结果毫无意义但不报错。

if / elif / else

score = 85

if score >= 90:
    print("优秀")
elif score >= 80:
    print("良好")
elif score >= 60:
    print("及格")
else:
    print("不及格")

三件事要注意:

  • 条件后面的冒号不能少
  • 分支体要缩进(4 个空格)
  • 从上往下第一个为真的分支被执行,其余全部跳过

最后一条很重要。上面的 elif score >= 80 不需要写成 elif 80 <= score < 90——能走到这一行,就说明 score >= 90 已经是假的了。

一个经典例题:闰年

规则是:能被 4 整除的是闰年,但能被 100 整除的不是,除非还能被 400 整除。

def is_leap(year):
    if year % 400 == 0:
        return True
    if year % 100 == 0:
        return False
    return year % 4 == 0

for y in [1900, 2000, 2024, 2025, 2026]:
    print(y, is_leap(y))

注意这里的写法:从最特殊的条件开始判断,一路 return 出去。这比写成嵌套的 if-else 清楚得多:

# 同样的逻辑,嵌套写法——能用,但读起来累
def is_leap_nested(year):
    if year % 4 == 0:
        if year % 100 == 0:
            if year % 400 == 0:
                return True
            else:
                return False
        else:
            return True
    else:
        return False

print(is_leap_nested(1900), is_leap_nested(2000), is_leap_nested(2024))

也可以一行写完,但可读性下降:

def is_leap_oneline(year):
    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

print(is_leap_oneline(1900), is_leap_oneline(2000), is_leap_oneline(2024))

三元表达式

只有两种情况、而且都很短的时候,可以写成一行:

score = 75
result = "及格" if score >= 60 else "不及格"
print(result)

# 等价于
if score >= 60:
    result = "及格"
else:
    result = "不及格"
print(result)

格式是 值1 if 条件 else 值2。别嵌套它——a if p else (b if q else c) 已经该换成 if-elif-else 了。

match-case:结构化模式匹配

Python 3.10 引入的语法,用来替代一长串 elif:

def http_message(code):
    match code:
        case 200:
            return "OK"
        case 301 | 302:            # | 表示"或"
            return "重定向"
        case 404:
            return "未找到"
        case 500:
            return "服务器错误"
        case _:                     # _ 是通配符,相当于 else
            return "未知状态"

for c in [200, 302, 404, 500, 418]:
    print(c, http_message(c))

它比 if-elif 强的地方在于能匹配结构,而不只是比较值:

def describe(point):
    match point:
        case (0, 0):
            return "原点"
        case (0, y):
            return f"在 y 轴上,y={y}"
        case (x, 0):
            return f"在 x 轴上,x={x}"
        case (x, y):
            return f"普通点 ({x}, {y})"
        case _:
            return "不是一个二维点"

for p in [(0, 0), (0, 5), (3, 0), (2, 7), "hello"]:
    print(p, "→", describe(p))

注意 case (0, y) 做了两件事:检查第一个元素是不是 0,并且把第二个元素绑定到变量 y。这是它和 if 的本质区别。

循环

while:条件为真就一直做

n = 1
while n <= 5:
    print(n, end=" ")
    n += 1
print()

while 的三个要素,缺一个就出问题:

  1. 初始化:n = 1
  2. 条件:n <= 5
  3. 更新:n += 1

for:遍历一个集合

for ch in "Python":
    print(ch, end=" ")
print()

for x in [10, 20, 30]:
    print(x, end=" ")
print()

in 后面可以跟任何”可以逐个取出元素”的东西——字符串、列表、元组、字典、集合。L13 会讲这背后的机制(可迭代对象)。

range()

需要”循环 n 次”的时候用 range():

print(list(range(5)))          # 0 到 4
print(list(range(2, 8)))       # 2 到 7
print(list(range(0, 10, 3)))   # 步长 3
print(list(range(10, 0, -2)))  # 倒着走

range(a, b) 是 含头不含尾——和切片一样的约定。

total = 0
for i in range(1, 101):
    total += i
print("1 加到 100 =", total)

for 还是 while

用 for用 while
知道要循环多少次不知道要循环多少次
要遍历一个已有的集合循环到某个条件满足为止
大多数情况读用户输入直到合法、迭代逼近某个精度

能用 for 就用 for——它的次数是写死的,不会忘记更新,也就不会死循环。

# 3n+1 问题(考拉兹猜想):不知道要几步,用 while
def collatz_steps(n):
    steps = 0
    while n != 1:
        if n % 2 == 0:
            n = n // 2
        else:
            n = 3 * n + 1
        steps += 1
    return steps

for start in [6, 7, 27]:
    print(f"从 {start} 出发,{collatz_steps(start)} 步到 1")

从 27 出发要走 111 步——这个数列会先冲到 9232 再掉下来。至今没人能证明它对所有正整数都会回到 1。

break 和 continue

  • break:立刻跳出整个循环
  • continue:跳过本轮剩下的部分,进入下一轮
# break:找到第一个就停
for i in range(2, 50):
    if 100 % i == 0:
        print("100 的第一个大于 1 的因数是", i)
        break
# continue:跳过不想要的
for i in range(1, 11):
    if i % 3 == 0:
        continue
    print(i, end=" ")
print()

for-else 和 while-else

这是 Python 特有的语法,很多人写了几年也不知道。

else 在循环”正常结束”时执行;如果是被 break 打断的,就不执行。

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True

print([n for n in range(2, 30) if is_prime(n)])

上面用了早返回。用 for-else 可以这样写:

def find_divisor(n):
    for i in range(2, n):
        if n % i == 0:
            print(f"{n} 能被 {i} 整除")
            break
    else:
        print(f"{n} 是质数")

find_divisor(15)
find_divisor(17)

位运算

L2 说过一切都是 bit。位运算符直接操作这些 bit:

运算符名称作用
&按位与两位都是 1 才是 1
|按位或有一位是 1 就是 1
^按位异或两位不同才是 1
~按位取反0 变 1,1 变 0
<<左移整体左移,右边补 0
>>右移整体右移
a, b = 0b1100, 0b1010     # 12 和 10

print(f"a     = {a:04b}")
print(f"b     = {b:04b}")
print(f"a & b = {a & b:04b}  = {a & b}")
print(f"a | b = {a | b:04b}  = {a | b}")
print(f"a ^ b = {a ^ b:04b}  = {a ^ b}")

左移一位等于乘 2,右移一位等于整除 2:

n = 5
print(n, "<< 1 =", n << 1)     # 10
print(n, "<< 3 =", n << 3)     # 40
print(n, ">> 1 =", n >> 1)     # 2

一个实用技巧——判断奇偶:

for n in [7, 8, 9, 10]:
    print(n, "奇" if n & 1 else "偶")

n & 1 取最低位:最低位是 1 就是奇数。这和 n % 2 == 1 等价,但更快(不过在 Python 里差别很小,可读性优先,用 % 就好)。

函数进阶

L3 讲了函数的基本形态。这里补上参数的各种玩法。

默认参数

给参数一个默认值,调用时可以不传:

def greet(name, greeting="你好"):
    return f"{greeting},{name}"

print(greet("张三"))
print(greet("张三", "早上好"))

有默认值的参数必须放在没默认值的后面,否则语法错误:

def bad(greeting="你好", name):
    return f"{greeting},{name}"

关键字参数与位置参数

def describe(name, age, city):
    return f"{name},{age} 岁,来自{city}"

print(describe("张三", 18, "上海"))                       # 按位置
print(describe(name="张三", age=18, city="上海"))          # 按名字
print(describe("张三", city="上海", age=18))               # 混用

混用时位置参数必须在前。这条规则的意义是:Python 得先知道哪些是按位置对应的,剩下的才能按名字认领。

*args 和 **kwargs

参数个数不确定的时候用它们:

def total(*numbers):          # * 把所有位置参数收集成一个元组
    print("收到:", numbers, type(numbers))
    return sum(numbers)

print(total(1, 2, 3))
print(total(1, 2, 3, 4, 5))
print(total())
def config(**options):        # ** 把所有关键字参数收集成一个字典
    print("收到:", options)
    for key, value in options.items():
        print(f"  {key} = {value}")

config(debug=True, level=3, name="test")

两个可以一起用,顺序固定是 def f(普通参数, *args, **kwargs):

def show(first, *rest, **opts):
    print("first =", first)
    print("rest  =", rest)
    print("opts  =", opts)

show(1, 2, 3, mode="fast", retry=2)

args 和 kwargs 只是习惯用名,* 和 ** 才是语法。但请沿用这两个名字——所有人都这么写,换名字只会让读者困惑。

同名函数会覆盖

Python 里没有函数重载。后定义的同名函数会直接盖掉前面的:

def f(x):
    return x * 2

def f(x, y):
    return x + y

print(f(3, 4))
def f(x):
    return x * 2

def f(x, y):
    return x + y

print(f(3))        # 原来那个单参数的 f 已经没了

这和 C++、Java 不同。Python 里想”根据参数个数做不同的事”,用默认参数或 *args。

类型提示

是什么

从 Python 3.5 起,可以给参数和返回值标注类型:

def c_to_f(c: float) -> float:
    return c * 9 / 5 + 32

print(c_to_f(37))

c: float 说明参数应该是浮点数,-> float 说明返回浮点数。

Python 不会强制检查它们。 你传个字符串进去照样会跑(然后报错):

def c_to_f(c: float) -> float:
    return c * 9 / 5 + 32

print(c_to_f("37"))

那为什么要写

既然不强制,为什么值得多敲这些字?三个理由,一个比一个重要:

一、它是给人看的文档,而且不会过期。 注释会和代码脱节,类型标注不会——它就在参数旁边。

二、编辑器能用它帮你。 VS Code 装了 Python 扩展之后,有类型标注的函数会有更准的补全和更早的错误提示。很多低级错误在你敲代码的时候就被标红了,不用等运行。

三、它是你和 AI 沟通的规格说明。 这一条到 L15 会展开讲,但现在可以先建立意识:当你让 AI 写一个函数,最有效的做法不是描述一段话,而是先把函数签名和类型写出来,让它填实现。

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """把两个已排序的整数列表合并成一个有序列表。"""
    ...

这三行比一百字的自然语言描述更精确——输入是什么、输出是什么、行为是什么,全在里面了。

常用写法

def f1(x: int) -> str:
    return str(x)

def f2(items: list[int]) -> int:
    return sum(items)

def f3(name: str, age: int = 18) -> str:      # 默认值和类型可以一起写
    return f"{name} is {age}"

def f4(x: float) -> None:                      # 没有返回值就标 None
    print(x)

print(f1(42), f2([1, 2, 3]), f3("Ada"))
f4(3.14)

缩进:最容易犯的低级错误

Python 用缩进划分代码块,这是它简洁的代价:缩进错了,程序的含义就变了,而且往往不报错。

def check(scores: list[int]) -> None:
    for s in scores:
        if s >= 60:
            print(s, "及格")
    print("检查完毕")          # 在 for 外面,只打印一次

check([50, 70, 90])
def check(scores: list[int]) -> None:
    for s in scores:
        if s >= 60:
            print(s, "及格")
        print("检查完毕")      # 缩进多了一级,每轮都打印

check([50, 70, 90])

两段代码只差四个空格,行为完全不同,都不报错。

小结

  • 顺序 + 分岔 + 重复,足以表达任何可计算的过程
  • if-elif-else 从上往下取第一个为真的分支;多路分支要从最严格的条件开始写
  • 链式比较 1 < x < 10 可以用,而且中间值只算一次
  • match-case 能匹配结构并绑定变量,但别过度使用
  • 能用 for 就用 for;while 留给”不知道要循环几次”的情况
  • break 跳出整个循环,continue 跳过本轮;嵌套里 break 只跳一层
  • 循环的 else 意思是”没被 break 打断”——看得懂就行,自己少写
  • 位运算和比较混用一定要加括号
  • 默认参数放在后面,且不要用可变对象做默认值
  • Python 没有函数重载,同名函数直接覆盖
  • 类型提示不强制,但值得写:文档、编辑器支持、以及给 AI 的规格说明
  • 统一用 4 个空格缩进,不要混 Tab

练习

  1. 写 bmi_category(bmi: float) -> str,按 <18.5 偏瘦、<24 正常、<28 偏胖、其余肥胖分类。注意分支顺序。
  2. 不用 sum(),用循环求 1 到 100 中所有能被 3 或 5 整除的数之和。
  3. 写 count_vowels(s: str) -> int,统计字符串里元音字母(aeiou,大小写都算)的个数。
  4. 用嵌套循环打印九九乘法表的下三角,格式对齐。
  5. 下面两段代码输出不同,解释为什么:
    for i in range(5):
        if i == 3:
            break
    else:
        print("完整跑完")
    
    for i in range(5):
        if i == 3:
            continue
    else:
        print("完整跑完")
  6. 写 apply_all(*funcs, value),把 value 依次传给每个函数处理,返回最终结果。 例如 apply_all(abs, int, value=-3.7) 应该返回 3。想一想为什么 value 必须用关键字传。
  7. 给你在 Lab 3 里写的所有函数补上类型提示。

本讲的配套上机题在 Lab 4。