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