系列教程更新到第四篇,前几篇里我们把Python的环境搭建、变量与数据类型、输入输出和运算符都过了一遍。有读者留言说,跟着教程装好Python、能跑通print之后,下一步就不知道该学什么了——这很正常,因为到目前为止,你写的代码都是从上到下一行一行往下走的,程序不会“思考”。从这篇开始,程序终于拥有了最简单的智能:根据不同的情况,走不同的路。这就是流程控制语句,具体来说,是if...elif...else和match...case。
这篇文章会从最基础的概念讲起,一步步拆解分支语句的语法、原理和实战用法,哪怕你是零基础、刚折腾完python安装的新手,也能跟着写完并跑通。后面还会讲到我用Python这些年踩过的一些坑,以及条件判断到底怎么写才又清晰又不容易出bug。
1. 流程控制语句为什么是Python程序的“岔路口”
1.1 没有流程控制的代码长什么样
先回忆一下前几篇的代码。不管是变量赋值、字符串拼接还是print输出,代码都是按顺序往下执行的,就像一条单行道:
name = "张三" age = 18 print(name) print(age)这种结构叫“顺序结构”。它本身没什么问题,但如果程序只能这么写,那它什么事也干不了。你想写一个“根据分数判断是否及格”的功能都不行,因为程序没有“判断”的能力。判断,就是流程控制里最核心的分支逻辑。
我在带新手的时候经常会打一个比方:程序就像你开车出门,顺序结构是一条没有分岔的高速路,你只能一直往前开。而真实世界的导航不是这样的——前方堵不堵、目的地停车位够不够、要不要绕路,都得在岔路口做选择。流程控制语句就是程序里的那个岔路口。
1.2 分支、循环与顺序:三大基本结构的定位
计算机科学的祖师爷早就总结过,任何复杂的程序逻辑都可以由三种基本结构组合出来:顺序、分支、循环。
- 顺序:从上往下逐行执行,这是所有程序的地基。
- 分支:根据条件成立与否,决定走哪一段代码,也就是if语句、match语句干的事。
- 循环:满足条件时重复执行一段代码,对应for和while。
今天这篇主讲分支。循环语句我会放到后面专门写一篇,但分支学好了,循环里的很多判断逻辑你才能顺手接住。
有人可能会问:既然有if,为什么Python 3.10还要出match...case?这不重复吗?这个问题的答案要放到后面讲match的时候详细展开。简单说,if擅长处理“区间判断”和“复合条件”,而match擅长处理“离散值分发”和“结构匹配”。两者互补,不是替代关系。
2. if...elif...else基础语法:从单分支到多分支一次讲清
2.1 最简单的if单分支结构
if单分支是最基础、最常用的写法。它做的事情很简单:条件成立,执行里面的代码;不成立,跳过。
age = 20 if age >= 18: print("你已经成年了")运行结果会打印“你已经成年了”。如果把age改成16,这个程序什么都不会输出。
这里有两个关键的语法细节,新手第一次写特别容易错:
第一,条件后面必须加英文冒号(:)。很多从其他语言转过来的朋友会不习惯,总觉得少了什么。这个冒号是Python语法的固定组成部分,表示“下面是一个代码块”。
第二,if下面的代码必须缩进。Python用缩进来划分代码块的层级,而不是像C语言、Java那样用花括号。一般约定缩进4个空格。如果你不缩进,运行时会直接报IndentationError。
我建议在编辑器里把Tab键自动替换成空格。Pycharm、VS Code都有这个设置,把“Tab and Indents”里的“Use tab character”取消勾选即可。原因很简单:空格和Tab混用会引发非常诡异的缩进错误,而且肉眼几乎看不出来。
2.2 双分支与多分支:if...else和if...elif...else
单分支只能处理“成立做什么,不成立什么都不做”的场景。但现实里更常见的是非黑即白:及格还是不及格,成年还是未成年。这时候需要else。
age = 16 if age >= 18: print("你已经成年了") else: print("你还未成年")else不需要写条件,它表示“上面的条件都不成立时,执行这里的代码”。
那三个及以上分支呢?比如成绩等级:优秀、良好、中等、及格、不及格。这时候就用elif。“elif”是“else if”的缩写,写法上合并成一个词,简化了嵌套层级,也更易读。
score = 85 if score >= 90: print("优秀") elif score >= 80: print("良好") elif score >= 70: print("中等") elif score >= 60: print("及格") else: print("不及格")这段代码的运行逻辑是这样的:从上往下依次判断,哪个条件先成立就执行哪个分支,后面所有分支不再执行。除此之外,elif可以写很多个,但else最多一个,而且必须放在最后。
有个非常重要的点,我每次讲课都会强调:分支条件的顺序是有讲究的。上面这段代码能正常工作,是因为我们按从高到低的顺序判断。如果你反过来写:
if score >= 60: print("及格") elif score >= 80: print("良好")那85分只会打印“及格”,因为第一个条件已经成立了,后面的elif根本没机会执行。这种错误叫“短路过早”,运行不会报错,但结果就是错的,是最隐蔽的逻辑bug之一。后面第6章我还会专门列一个排查清单。
2.3 条件表达式到底在判断什么
if后面那些条件,本质上都在计算一个布尔值:True或者False。
比较运算是最常见的条件来源。Python支持的比较运算符包括:
| 运算符 | 含义 | 示例 |
|---|---|---|
| == | 等于 | age == 18 |
| != | 不等于 | age != 18 |
| < | 小于 | score < 60 |
| > | 大于 | score > 90 |
| <= | 小于等于 | age <= 17 |
| >= | 大于等于 | age >= 18 |
注意,判断相等是两个等于号(==),一个等于号(=)是赋值。这个坑几乎所有Python新手都踩过,我后面单独讲。
多个条件组合时,用逻辑运算符:and(且)、or(或)、not(非)。优先级从高到低是not > and > or。
age = 20 has_ticket = True if age >= 18 and has_ticket: print("可以进场")这一段表示两个条件同时成立才执行。“and用于同时满足,or用于满足其一”这个理解很直观。但有件事新手容易忽略:Python的条件判断不完全是True/False两端点,它还遵循“真值测试”规则——任何对象都可以直接用作条件。
在if判断中,以下几种情况会被当成False:
- 布尔值False
- 数字0和0.0
- 空字符串""
- 空列表[]、空元组()、空字典{}和空集合set()
- None
其他所有值都被当成True。所以你可以直接写:
name = input("请输入名字:") if name: print("你好,", name) else: print("名字不能为空")这比写“if name != ""”更简洁、更Pythonic。很多老手写代码时都喜欢利用这个真假值特性。
3. if条件判断的进阶经验:别把代码写成“俄罗斯套娃”
3.1 条件太复杂时,先拆变量再判断
新手的代码里经常能看到这样的条件:
if user is not None and user.status == "active" and user.age >= 18 and user.city == "北京": print("允许进入")一行代码四五个条件,看得人头皮发麻。如果某个条件不满足,排查的时候得一个个拆开试,效率极低。
我会建议把复杂的条件拆出来,赋给有意义的变量名:
is_active_user = user is not None and user.status == "active" is_adult = user.age >= 18 is_in_beijing = user.city == "北京" if is_active_user and is_adult and is_in_beijing: print("允许进入")这段代码逻辑完全一样,但可读性天差地别。每个条件是什么一目了然,后续如果要改条件,直接改变量定义那一处就行。这是实战中非常实用的优化技巧,看起来没加多少代码,但维护体验完全不一样。
3.2 三元表达式:简化简单的if...else赋值
Python里有一种简洁的写法叫三元表达式(也叫条件表达式),语法是“值1 if 条件 else 值2”。条件成立取值1,否则取值2。
age = 20 status = "成年" if age >= 18 else "未成年" print(status)这个写法的适用场景非常明确:if和else里都只是简单的赋值或返回,没有复杂逻辑。一句话就能写完,代码干净。
但三元表达式有个大忌:不能滥用。如果你在表达式里嵌套另一个三元表达式,或者把很长的函数调用写进去,可读性会迅速恶化。比如:
result = "优秀" if score >= 90 else "良好" if score >= 80 else "中等"这种链式三元虽然能跑,但阅读时需要花很多精力去解析优先级,远不如用if...elif...else清晰。我的原则是:只有单个if...else的场景才用三元表达式;多分支场景一律用传统写法。
3.3 嵌套if一定不好吗?不一定
很多人听过“嵌套if影响可读性”的说法,于是遇到任何情况都强行用and合并。其实这不一定是对的。
比如逻辑本身就有明确的先后顺序:
if user: if user.is_admin: print("管理员界面") else: print("普通用户界面") else: print("请先登录")这种嵌套是有意义的:先判断“用户是否存在”,再判断“是否管理员”,两个问题的层次不同。如果硬要合并成一个条件“if user and user.is_admin”,就丢失了“用户不存在”这一层信息,else分支就没法精确处理未登录的情况了。
再举一个提前返回的例子,这个在函数里特别常用:
def process_order(order): if order is None: return "订单为空" if not order.is_paid: return "订单未支付" # 真正的业务处理 print("处理订单") return "处理完成"这种写法把异常情况前置,每个if只负责一个边界判断,主流程顺畅地留在最后。阅读体验非常好,也不存在多层嵌套的问题。
所以我的建议是:嵌套本身不是问题,问题是“无意义的深层次嵌套”。如果嵌套层级超过三层,通常说明逻辑设计可以优化,要么调整结构,要么抽成函数。如果继续深挖下去,代码的复杂度会指数级增长,那不是缩进风格能救回来的。
4. match...case结构化模式匹配:Python 3.10带来的新解法
4.1 match...case到底解决了什么问题
Python 3.10引入了一个新特性叫结构化模式匹配,语法关键字是match...case。很多人一看到它就说“Python终于有switch了”,这种理解只对了一半。它跟其他语言里的switch确实有点像——都是拿一个值去跟多个分支比较——但match的能力比switch强得多。
switch只能做等值比较,match能做结构匹配。这句话怎么理解?等值比较是“这个值等于多少”,结构匹配是“这个数据长什么样”。它可以匹配元组、列表、字典,甚至对象的属性,还能把匹配到的部分直接提取出来赋值给变量。这就很厉害了。
它主要解决的是if...elif...else链在一些特定场景下的冗长问题。比如要根据用户的命令字符串分发到不同处理逻辑,用if写会有一长串“xxx == 'start'”这种重复代码,用match写则干净很多。
4.2 基本语法与最简单的用法
match...case的基本结构是这样的:
command = "start" match command: case "start": print("启动服务") case "stop": print("停止服务") case "restart": print("重启服务") case _: print("未知命令")运行时会拿command的值去依次跟每个case后面的“模式”做匹配,命中了就执行对应代码,然后整个match语句结束。这里的下划线“_”是通配符,等价于if里的else,表示“什么都不匹配时走这里”。
有几个细节必须注意:
第一,case后面不需要写break。其他语言里的switch如果漏写break会发生“穿透”,但Python的match设计成匹配成功后自动终止,从根源上避免了这个问题。
第二,_符号不是变量,它只是一个通配标记,表示“我不关心这里是什么值”。
第三,一定要给match写兜底分支。如果所有case都没匹配到,而且没有case _,程序不会报错,但它会静默地什么都不做。这种静默失败非常坑,调试的时候很难发现。
4.3 进阶玩法:从等值比较到结构匹配
match真正拉开差距的地方在于它能匹配“结构”。先说最简单的组合模式:一个case里可以用“|”表示多个值都匹配。
code = 404 match code: case 404 | 500: print("服务器错误") case 200: print("请求成功") case _: print("其他状态码")然后是序列模式:匹配列表或元组的结构,并提取元素。
point = (3, 5) match point: case (0, 0): print("这个点位于原点") case (x, y): print(f"点的坐标是({x}, {y})")这里不需要提前写x = point[0]这种赋值,case直接帮你解包了。对于处理坐标、二元组这类结构,这个能力非常方便。
甚至可以匹配字典结构,这在处理JSON数据时很实用:
data = {"name": "张三", "age": 18} match data: case {"name": name, "age": age}: print(f"名字是{name},年龄是{age}") case _: print("数据格式不正确")匹配成功后,name和age这两个变量会自动绑定到字典里对应的值。
还有守卫条件,就是在case模式后面加一个if,给匹配再加一道关卡:
score = 85 match score: case _ if score >= 90: print("优秀") case _ if score >= 80: print("良好") case _ if score >= 60: print("及格") case _: print("不及格")这里“case _”表示“任何值都匹配”,后面的if再进一步条件判断。这就是match处理区间判断的通用写法。不过老实说,类似这种区间分级的需求,用if...elif...else会更自然一点,这一点我后面实战部分会细说。
4.4 match和if应该怎么选
写代码碰到分支,选if还是match?我根据自己的实际经验,总结了这么几个判断标准:
第一,如果是离散的等值判断,且分支数量比较多,用match更清晰。典型场景是状态机、命令分发、菜单选项。
第二,如果是区间判断、范围判断,比如分数等级、年龄分段,用if更顺手。match写守卫条件虽然能实现,但可读性不如if直观。
第三,如果需要同时判断多个变量之间的关系,或者在判断时还想提取数据结构里的内容,match的优势就很明显。if写这个会非常啰嗦。
第四,版本兼容性要心里有数。match语法是Python 3.10才有的,如果你的运行环境是3.9或更早,强行写会直接报语法错误。至于怎么确认版本,运行python --version就行。
| 对比项 | if...elif...else | match...case |
|---|---|---|
| 适用场景 | 区间判断、复合条件、真假值判断 | 离散值分发、结构匹配 |
| 可读性 | 条件复杂时更好读 | 分支多且是等值比较时更清晰 |
| 输入结构提取 | 不支持,得手动解析 | 支持序列、字典的解包提取 |
| 版本要求 | 所有Python版本 | Python 3.10及以上 |
| 语法风格 | 贴近自然语言,灵活 | 更像函数式语言里的模式匹配 |
所以我一般跟学员说,先把if用熟练,match是锦上添花。但既然语言提供了这么好用的语法糖,该用的时候就应该用起来,前提是你清楚它的边界在哪里。
5. 实战案例:成绩等级与命令分发器的两种写法对比
5.1 场景一:成绩等级转换
需求很简单:输入一个0到100的分数,输出对应等级。90分及以上为“优秀”,80到89为“良好”,70到79为“中等”,60到69为“及格”,60以下为“不及格”。
用if来实现:
score = int(input("请输入分数:")) if score >= 90: print("优秀") elif score >= 80: print("良好") elif score >= 70: print("中等") elif score >= 60: print("及格") else: print("不及格")这段代码逻辑非常清晰,从上往下依次判断,每个区间的边界条件一目了然。这里有个容易忽略的细节:int(input(...))把用户输入转换成整数了。如果你直接拿字符串跟数字比较,会报类型错误。
用match来实现:
score = int(input("请输入分数:")) match score: case _ if score >= 90: print("优秀") case _ if score >= 80: print("良好") case _ if score >= 70: print("中等") case _ if score >= 60: print("及格") case _: print("不及格")两者都能运行,结果完全一致。但你看这个例子,match版本并没有比if版本更简洁,甚至还要多理解一层守卫条件的写法。这种情况下我明确推荐用if——区间判断本来就是它的主场。
5.2 场景二:命令行工具的分发器
再看另一个场景:做一个简单的命令行工具,用户输入start、stop、restart、status时,程序执行对应的功能。
用if来写:
command = input("请输入命令:").strip().lower() if command == "start": print("服务启动中...") elif command == "stop": print("服务已停止") elif command == "restart": print("正在重启服务") elif command == "status": print("服务运行中") else: print("不支持的命令")这段代码不丑,但你会发现每个elif都要重复写一遍“command == ”,分支越多,重复越明显。如果以后要加十几个命令,这个函数会变得又长又啰嗦。
用match来写:
command = input("请输入命令:").strip().lower() match command: case "start": print("服务启动中...") case "stop": print("服务已停止") case "restart": print("正在重启服务") case "status": print("服务运行中") case _: print("不支持的命令")对比非常直观。match直接把command这个变量“摆”在最前面,每个case只需要写一个字符串常量。少了重复的等值判断,代码密度更高,也更干净。
这个例子就能看出两者的定位差异:当判断逻辑只是“变量等于某个固定值”时,match就是更合适的选择。我在实际工作中处理类似“命令路由”的模块时,现在都会优先考虑match。
5.3 场景三:用match处理元组结构
最后给一个if很难写、但match非常轻松的场景:根据二维坐标判断位置。
point = (5, 8) match point: case (0, 0): print("原点") case (0, y): print(f"在Y轴上,y = {y}") case (x, 0): print(f"在X轴上,x = {x}") case (x, y): print(f"普通点:({x}, {y})")同样的逻辑用if写,你得先拆开point[0]、point[1],再依次判断是否为0、是否为原点,代码至少多写一倍,而且可读性差很多。match直接把元组结构写进case里,同时还把感兴趣的坐标值绑定成变量,一步到位。
这种结构匹配能力,才是match真正区别于传统switch的地方。它让代码描述的是“数据应该长什么样”,而不是“如果第0个元素等于...”这种机械流程。
6. 新手常踩的坑:语法错误、条件顺序与match陷阱排查笔记
6.1 最容易报错的五个语法问题
我在带新手的这几年里,发现大部分早期报错都集中在几个固定的地方。整理成一个速查表,方便你在出错时快速定位。
| 报错信息 | 常见原因 | 解决办法 |
|---|---|---|
| SyntaxError: invalid syntax | if条件后漏冒号,或误用单个=号 | 检查行尾冒号,检查条件里是否用了== |
| IndentationError: expected an indented block | if下方没有缩进代码 | 在if语句下方统一缩进4个空格 |
| IndentationError: unindent does not match any outer indentation level | 空格和Tab混用 | 关闭编辑器的Tab替换为空格,或者全部改成Tab,保持统一 |
| SyntaxError: expected ':' | elif或else前一行缺少冒号 | 逐行检查if/elif/else行尾是否有冒号 |
| SyntaxError: positional argument follows keyword argument | 括号忘写或参数顺序写错 | 仔细核对print等函数调用的括号 |
新手最常见的其实就三种:缺冒号、忘缩进、==写成=。每次运行报语法错误,优先检查这三处,命中率极高。
6.2 条件顺序错误:程序不报错但结果是错的
这是我在实战里踩过的最坑的一类问题。程序能正常运行,但输出跟你预期不同。典型例子:
score = 85 if score >= 60: print("及格") elif score >= 80: print("良好") elif score >= 90: print("优秀")我故意把条件顺序调乱了,结果就是85分只输出“及格”。原因前面讲过:if条件成立后,后面所有的elif都不会再判断。这种错误用语法检查工具根本发现不了,唯一的防线就是你自己在写分支的时候保持清醒——条件范围窄的、严格的要先写,范围宽的放后面。
另一个经典错误是边界值。比如判断“小于60不及格,60及以上及格”,如果你把60分划到了不及格那边,那就是边界值没处理好。测试的时候一定要专门测边界值:60、59、90、89、0、100,每一条边界都必须跑一遍。
6.3 match的隐藏陷阱:版本、捕获变量与兜底分支
match虽然好用,但有几个陷阱跟if完全不同,刚开始用很容易翻车。
第一坑:版本。在Python 3.9及以下版本写match语句,会直接报SyntaxError。这是语法层面的不支持,不是你的代码写错了。安装新一点的Python版本就能解决。
第二坑:case后面的变量是“捕获”而不是“比较”。我见过有人这样写:
target = "start" command = "stop" match command: case target: print("命中target")他本意是拿command和target的值比较,但实际上这个case几乎会匹配任何值,因为它把command的字符串“捕获”并绑定给了target这个变量。如果需要比较变量值,不能用这种写法,得用守卫条件:
match command: case _ if command == target: print("命中target")这个坑特别隐蔽,因为它不报错,只是逻辑不对。我建议match里的case后面尽量只写字面量或结构模式,不要指望它帮你做变量等值比较。
第三坑:漏写兜底分支。match的静默失败会让程序在“什么都没匹配”的情况下安静地继续往下走,你不容易察觉。我的习惯是每个match都写case _,哪怕只是打印一条调试日志,也能保证程序对未知情况有明确的响应。
6.4 几个排查思路的实操建议
如果你写的分支代码出了bug,光盯着屏幕看往往看不出问题。我更推荐用下面这几个方法调试:
一是插入临时print标记。在每个分支里加一行print("进入分支A"),运行后看打印了哪行,就能确认条件判断的实际走向。排查完后删掉这些临时代码即可。
二是用二分法注释代码。如果一段代码既长又复杂,先把一半注释掉,看问题是否还存在,通过这种方式快速缩小出问题的范围。这个方法不仅适用于分支,对所有代码调试都管用。
三是把条件抽取成变量,一行一行检查。复杂条件拆成变量后,你可以在每个变量下面加print,看看它到底算出来是True还是False,判断逻辑哪个环节出了问题一目了然。
我个人在实际操作中的一个体会是:流程控制语句本身非常简单,难点永远在“条件怎么组织”和“顺序怎么安排”上。把分支的条件当成你平时写需求时的思考过程,而不是当成语法任务来背,很多逻辑问题就会自然地解决。如果你在这篇文章里只能记住一句话,那我希望是这句:写完分支逻辑后,一定要测边界值、测异常输入、测每个分支至少一次——这个过程能帮你避开绝大多数隐藏的坑。