我接手过不少新人的 Python 代码发现很多人不是倒在了变量、列表这些基础上而是卡在了流程控制这一关。变量学得再熟条件判断一复杂、循环一嵌套代码就乱成一锅粥。其实这不是智商问题而是没搞懂一件事流程控制语句就是程序的分岔路口程序执行到这儿是往左还是往右是循环三遍还是直接跳出全靠这几行关键词说了算。作为“Python 核心语法”系列的第二篇这篇把条件判断、循环结构、break/continue/else、match 模式匹配这些流程控制的关键点一次性讲透配合踩坑实录和可直接抄的实战代码不管是零基础入门、还是学过一遍想系统梳理的人都能在这篇文章里找到自己需要的东西。1. 内容整体设计与思路拆解1.1 流程控制为什么是 Python 学习的“分水岭”你先想一个问题一段只靠顺序执行的程序能干什么答案是什么也干不了。程序之所以能解决实际问题靠的就是“根据条件选择不同分支”“重复执行某段逻辑”这两件事而这两件事统称流程控制。在 Python 里流程控制主要分成三类顺序结构、条件分支if/elif/else、match、循环结构for、while。顺序结构没什么好讲的从上往下写就行真正考验人的是后两者。很多新手学到循环时特别容易一头雾水最核心的原因是他们习惯把代码当成“从上到下念一遍”的文字而不是“会跳来跳去的执行流”。你可以把程序想象成一个迷宫里的角色顺序结构是一直直走条件分支是遇到岔路口选择路线循环则是绕着某一圈反复走直到满足出口条件。一旦你对“程序是跳着执行的”建立了画面感流程控制就成功了一半。我在带新人的时候常说一句话数据结构和流程控制是编程的两条腿。变量、列表、字典这些是“积木”流程控制是“胶水”把积木按你想要的逻辑粘起来。没有流程控制你有再多积木也只能堆成一团拼不出任何有实际功能的东西。这也是为什么 Python 自学教程里流程控制总是排在数据类型、运算符之后的位置——它是第一个真正考验“编程思维”的关卡。1.2 和其他语言相比Python 流程控制特别在哪如果你之前接触过 C、Java 这类语言你会发现在它们里面代码块是靠着花括号{}圈起来的缩进只是为了让代码好看。但在 Python 里缩进本身就是语法的一部分。少了缩进解释器直接报IndentationError缩进不一致程序甚至会跑出完全不同的逻辑结果而且很难排查。这种设计有好有坏。好的一面是强制缩进让代码非常整洁团队协作时不可能出现那种“花括号满天飞、谁也找不到函数边界”的情况。坏的一面是它确实劝退了一部分从 C 系语言转过来的老手很多人一上来就习惯性地敲花括号结果发现 Python 根本不认这套。我自己见过一个从 Java 转 Python 的同事写了两周之后才彻底把“右花括号”这个肌肉记忆给戒掉。另外还有一个容易忽略的点Python 的条件和循环语句后面都要加冒号:。这个冒号是语法的一部分少了它直接SyntaxError。很多新手写if score 60后面忘了冒号报错了还反过来怀疑是不是缩进不对其实先把冒号检查一遍至少能消掉三成的基础错误。1.3 这篇内容的结构和适合人群这一篇的整体安排是这样的先拆条件判断从最基础的if/elif/else到真值判断陷阱再到 Python 3.10 才引入的match模式匹配然后拆循环把for、range、enumerate、zip、while、break/continue/else这条线完整过一遍接着用两个实战案例把前面这些零散知识点串起来最后整理一份我在日常调试里遇到频率最高的流程控制相关报错和逻辑问题。适合读这篇内容的人有两类。第一类是刚学完变量、列表、元组、字典这些基础语法正准备迈入流程控制的新手你可以沿着文章顺序一步一步来每段代码都自己敲一遍再往下读。第二类是已经学过一遍 Python、但写自己的小工具时仍然觉得逻辑不顺的人你可以重点看第 2、3 章的知识点复盘以及第 5、6 章的踩坑经验这会比你从头再刷一遍教程高效得多。2. 条件判断从 if 到 match 的完整打法2.1 if / elif / else 的基本结构与缩进规则条件判断最基础的写法就是if/elif/else语法结构很简单score 85 if score 90: print(优秀) elif score 60: print(及格) else: print(不及格)这里有几个关键点要理解清楚。第一条件是从上往下逐个判断的执行到第一个满足条件的分支后后面的elif和else都不会再被检查。比如score 95程序进入if score 90这个分支后输出“优秀”这时根本不会再去看elif score 60哪怕它也满足。这个特性非常重要它意味着你写条件的顺序必须是从“严格”到“宽松”否则会出逻辑错误。比如你把elif score 60放在if score 90前面那score 95也会走进“及格”这个分支因为95 60先被满足了。第二每一个条件语句块都通过缩进来识别。习惯上同一层级的代码块用四个空格做缩进这是 PEP 8 的官方建议。这里有个实用建议不管你用什么编辑器把 Tab 键自动替换成空格。我在 PyCharm 和 VS Code 里都是这样配置的目的就是为了彻底避开后面第 5 章要讲的“混用 Tab 与空格”的经典大坑。第三elif是 Python 对“else if”的缩写。你写多少个elif都行只要保持同一个缩进层级即可。在实际开发中如果分支特别多而且每个分支就是在做纯值的匹配那么用后面 2.3 节要讲的match语句往往更清晰。2.2 条件表达式里的“真值判断”陷阱这一节我想专门拎出来讲因为是我见过的大坑集中区。在 Python 里一个表达式放在if后面最终会被当成“真值”来判断这里遵循的规则是以下这些值会被判定为False——0、0.0、空字符串、[]空列表、()空元组、{}空字典、set()空集合、None。除此之外的其他值哪怕是一个只有一个元素的列表也会被判定为True。这个特性本身很好用可以让代码更加简洁但你得真的理解它才行。比如判断一个列表是不是空的很多人会写if len(items) 0: print(列表是空的)其实更地道的写法是直接判断列表本身if not items: print(列表是空的)这两者效果完全一样但后者更 Pythonic读起来也更自然。反过来说判断一个列表有内容时直接写if items:即可不需要写if len(items) 0:。真正容易出问题的地方在于当你把None和空列表混在一起判断时直接判断真值可能会导致误判。比如items可能是一个None也可能是一个空列表你希望这两种情况分别处理。如果你只写if not items:那么None和空列表都会走进这个分支你根本分不清到底是哪一种情况。这种时候就应该用is None或 []做显式判断不要图省事。还有一个更隐蔽的坑很多人习惯在条件里写if a None:虽然这能跑但 PEP 8 明确规定和None比较应该用is None而不是。原因在于会调用对象的__eq__方法某些自定义对象可能会在比较时返回令人意外的结果而is比较的是身份对None来说永远是最可靠的。建议你把if x is None写进肌肉记忆。另外紧凑的三元表达式也很常用它的格式是a if 条件 else b。比如status 成年 if age 18 else 未成年这个表达式等价于完整的if/else语句但更适合写在一行里给变量赋值。注意它有“先中间后两边”的阅读顺序刚接触时容易被绕晕但只要记住“条件成真返回 if 左边否则返回 else 右边”就不会搞混。2.3 用 match 做模式匹配Python 3.10Python 3.10 开始引入了match语句官方叫法是“结构化模式匹配”。这玩意儿不是简单的“switch-case 换皮”它能做更强大的模式解构。最基础的用法是对值进行精确匹配command start match command: case start: print(启动程序) case stop: print(停止程序) case restart: print(重启程序) case _: print(未知命令)这个写法的好处是代码非常整齐像是一张对照表。case _:相当于默认分支类似于其他语言里的default。需要注意的一点是match语句里的case分支并不会像 C 语言那样“穿透”匹配到某个分支执行完就直接结束整段match不需要写break。match还支持按结构解包。比如你想根据用户的“操作 目标”组合来分发任务action (move, 10, 20) match action: case (move, x, y): print(f移动到坐标 ({x}, {y})) case (click,): print(单击) case _: print(无法识别的操作)当action是一个元组且第一个元素是move时后面两个值会自动解包给x和y直接就能在分支里用。这个能力在处理协议解析、消息分发这类场景时特别顺手。不过我的建议是如果你只是做简单的值比较if/elif/else完全够用不一定非要上match。当你的分支条件涉及“结构判断值提取”或者分支条件特别多超过四五个时match的代码可读性优势才会真正体现出来。另外要注意match只支持 Python 3.10 及以上版本如果你的项目还在用 3.8 或 3.9用了这个语法会直接SyntaxError。3. 循环结构for 和 while 的正确打开方式3.1 for 循环与 range 的完整组合for循环在 Python 里专门用来遍历可迭代对象比如列表、元组、字符串、字典、集合、文件行等。基本写法是fruits [苹果, 香蕉, 橙子] for fruit in fruits: print(fruit)这里fruit是我们自己起的循环变量每次循环它会依次取到列表里的一个元素。循环体内部可以随便使用这个变量循环结束后它仍然保留最后一次的值不会像有些语言那样在循环外就不可见了。很多从其他语言转过来的新手喜欢用“下标遍历”的写法for i in range(len(fruits)): print(fruits[i])这种写法在 Python 里不是不行但不够地道。直接for fruit in fruits:明显更简洁而且可读性更好。只有当你在循环体里确实需要知道当前元素的下标时才应该去想下标的方案这就是 3.2 节要讲的enumerate派上用场的时候。range是 Python 内置的序列生成函数在for循环里配合使用极其常见。range(stop)生成0到stop-1的整数序列range(start, stop)生成start到stop-1range(start, stop, step)则额外指定步长步长可以为负数用于倒序。举个例子for i in range(1, 10, 2): print(i) # 输出 1 3 5 7 9注意range的区间是左闭右开也就是包含起点、不包含终点。这个规则在 Python 里到处都在用切片、列表索引也都是这个套路你只要记住“右侧边界取不到”这一个原则就不会在边界问题上反复踩坑。另外遍历字典的时候要分清楚几种方式直接for key in dict遍历的是键for key in dict.keys()也是键for value in dict.values()遍历值for key, value in dict.items()同时遍历键和值。日常写代码时最常用的是items()因为它一步到位不需要再通过键去取一遍值。3.2 enumerate 与 zip循环中的两个高级技巧写循环时真正高频使用的进阶工具我认为是enumerate和zip。enumerate的作用是给可迭代对象生成带索引的序列。它的典型用法是students [张三, 李四, 王五] for index, name in enumerate(students, start1): print(f第{index}名同学{name})start参数默认是 0你可以根据自己的业务需求指定从 1 开始或者从任何数字开始。在你需要“拿当前是第几个”的时候这个函数比手动维护计数器干净得多。如果你写下面这种代码说明你还在用 C 的思维写 Python需要赶紧改掉index 0 for name in students: print(name, index) index 1zip的作用是把多个可迭代对象按位置打包成元组序列。比如有两个列表你想按索引同时取元素names [张三, 李四] scores [88, 92] for name, score in zip(names, scores): print(f{name} 的分数是 {score})zip的打包长度以最短的可迭代对象为准多余的元素会被忽略。如果你希望按最长的对象循环不足的部分用None补齐可以用itertools.zip_longest。在数据处理场景里这个细节很关键我曾经在合并两份不同长度的日志时因为没有用zip_longest丢掉了末尾很多行数据排查了半天才找到原因。3.3 while 循环与退出条件的控制while循环的语法更简单它只在条件为真时不断执行循环体直到条件变为假才结束。基本写法count 3 while count 0: print(f倒计时{count}) count - 1while循环的关键难点在于“什么时候退出”必须非常明确。我经常看到新手写while循环时忘记在循环体里改变控制条件结果程序直接卡死。比如上面这个例子如果你把count - 1漏了count永远是 3while count 0永远为真循环就永远停不下来。在实际开发中while True配合break是很常见的一种模式特别适合“先执行再判断是否退出”的场景。比如用户输入命令行程序里最常见的交互while True: command input(请输入命令输入 quit 退出) if command quit: break print(f执行命令{command})这个写法的好处是不需要预先给循环条件一个初值可以把退出条件写在循环体中间逻辑特别自然。但使用这种写法时你必须确保break在某种情况下一定会被执行到否则就是无解的死循环。我个人的习惯是写while True之前先问自己一句“这个循环一定会退出吗”如果答不上来就不要用这种结构。while适合用在循环次数不确定、需要靠某个条件来控制的场景比如读文件直到结尾、等待某个外部状态变化。与之相对for循环更适合遍历已知的序列。两者没有绝对的优劣选对场景是关键。3.4 break、continue 与 for-else 的配套逻辑break和continue是循环体内部的两个“控制开关”。break表示立即结束整个循环不管循环条件是否还满足continue表示跳过本次循环的剩余语句直接进入下一轮循环。比如你在列表里寻找第一个偶数找到后就可以用break提前退出numbers [3, 7, 8, 11] target None for num in numbers: if num % 2 0: target num break print(target) # 输出 8这里一旦找到第一个偶数循环就中断了不再继续向后遍历效率明显更高。很多新手不知道break可以直接跳出循环非要用一个is_found标志位来控制代码反而更啰嗦。Python 循环还有一个非常特别的设计——for-else和while-else。它的执行逻辑是当循环正常结束也就是没有被break中断时会进入else块。这个语法很多人没见过但它在某些场景下特别好用比如判断一个数是不是质数num 17 for i in range(2, num): if num % i 0: print(f{num} 不是质数能被 {i} 整除) break else: print(f{num} 是质数)如果循环过程中某个i能整除num程序会执行break跳出循环这时else块不会执行如果整个循环都走完了也没有找到能整除的因子说明它是质数else块就会执行。这种“没有被中途打断就执行”的语义用for-else写出来逻辑非常紧凑。不过在实际项目里这种语法对可读性有一定要求团队里如果有人不熟悉很容易产生理解偏差所以使用前最好在注释里说明一下意图。4. 实操过程与核心环节实现4.1 写流程控制前先画“脑内流程图”我见过太多人在接到一个小需求时马上打开编辑器吭哧吭哧开始写写到一半发现逻辑不对再推翻重来。这种做法的效率其实非常低写流程控制相关的代码尤其如此。我的建议很简单动手写代码前先在脑子里或者草稿纸上把流程过一遍把下面四个问题想清楚入口是什么也就是这份数据或者程序从哪里开始读。有哪些条件分支每个分支的进入条件是什么条件之间有没有优先顺序哪些部分需要重复执行循环体里要处理的数据是什么循环的出口在哪里什么情况下循环会结束会不会出现永远结束不了的情况比如你要写一个程序从一堆学生成绩里统计及格人数和不及格人数。拿到需求后先想入口是成绩列表分支条件是score 60重复执行的是“读取每个成绩并进行判断”循环出口是“列表元素取完”。想清楚这四步之后代码几乎是一气呵成的事情。再简单一点的场景你可以先用中文伪代码把逻辑串一遍比如遍历成绩列表 如果 分数 60 及格人数 1 否则 不及格人数 1 输出统计结果等伪代码逻辑没问题了再对照着写真正的 Python 代码。这个习惯在你自己练手的时候可能觉得多此一举但一旦开始写几百行的脚本或者之后做项目级开发它的价值会立刻体现出来。4.2 实战用 Python 写一个“猜数字”小游戏流程控制的经典入门实战我推荐猜数字游戏。它把输入输出、条件分支、循环、break全占了代码量又小非常适合拿来练手和讲原理。我写的版本大致是这样的import random target random.randint(1, 100) attempts 0 while True: guess_text input(请猜一个 1 到 100 之间的数字) guess int(guess_text) attempts 1 if guess target: print(猜大了) elif guess target: print(猜小了) else: print(f恭喜猜对了你一共猜了 {attempts} 次。) break这段代码里random.randint(1, 100)是生成一个随机目标数attempts用来记录猜的次数while True表示循环没有被初始化条件限制靠内部的两个分支和break来控制何时退出。如果玩家猜的数字比目标大输出提示如果小也提示猜对了就输出结果并break结束循环。这里有个小问题int(guess_text)在用户输入了非数字时会直接报ValueError程序就崩了。想让这个小游戏更健壮可以在转换前加一个判断或者用try/except捕获异常这属于异常处理的范畴我们后面的篇章会讲到。但你现在就可以意识到一个问题真实程序里用户永远不会按你设想的路径操作流程控制不仅要管“正常流程”还要管“异常流程”。进阶的话可以给这个游戏加入难度选项、记录历史最佳成绩、限制最大猜测次数等功能。比如限制猜 10 次以内循环就变成了需要同时判断“是否猜中”和“是否超过次数”两个条件这才能真正检验你对多条件循环控制的理解。4.3 流程控制在数据清洗与爬虫场景中的典型应用流程控制不只是写给控制台小游戏用的它在数据清洗、爬虫、自动化脚本里都是核心逻辑。这里举两个我实际用过的片段。第一个是数据清洗场景。假设你有一个销售记录的列表每条记录是(商品名, 价格, 销量)你想筛选出“价格大于 100 且销量大于 50”的商品并给它们标记为“重点商品”这时候流程控制就体现了records [ (鼠标, 129, 80), (键盘, 399, 35), (显示器, 999, 60), (耳机, 89, 120), ] for name, price, sales in records: if price 100 and sales 50: print(f{name} 是重点商品价格 {price}销量 {sales}) elif sales 50: print(f{name} 销量偏低考虑促销)第二个是爬虫场景。写爬虫时长常见的一个问题是某个请求可能会失败需要重试几次。这个逻辑用for加break来实现非常清晰import time url https://example.com/api/data max_retries 3 for attempt in range(1, max_retries 1): print(f第 {attempt} 次请求) try: # response requests.get(url) # 模拟请求成功 print(请求成功) break except Exception: print(请求失败等待重试...) time.sleep(1) else: print(超过最大重试次数请检查网络)这段代码里break在成功时跳出循环让for-else的else块只有在三次都失败的情况下才触发。这种写法比用标志位判断“是否成功过”要简洁得多核心就是这个else块和break的天然搭配。5. 常见问题与排查技巧实录5.1 IndentationError缩进错误在所有流程控制相关的报错里出现频率最高的肯定是IndentationError。常见的触发原因有三个没有缩进、缩进量不一致、Tab 和空格混用。报错信息一般长这样IndentationError: expected an indented block after if statement on line 1它的意思是if语句后面需要一个缩进的代码块但你并没有缩进。新手最容易犯的错就是if score 60: print(及格) # 这里没有缩进直接报错解决方法非常简单在if冒号后面的行首加上四个空格。elif、else、for、while等语句后面同理。真正恶心的是 Tab 和空格混用。Python 3 里已经不允许同一个代码块中混用 Tab 和空格做缩进只要你混用了解释器就会报TabError: inconsistent use of tabs and spaces in indentation。最稳的解决办法是在编辑器里把 Tab 键统一替换成四个空格然后把“显示空白字符”的选项打开。在 VS Code 里你可以在设置项editor.renderWhitespace: all开启空白字符显示这样就能肉眼看出来哪些缩进是 Tab 哪些是空格。5.2 在循环中修改列表的诡异现象有一个很经典的问题在for循环遍历列表的时候如果你直接在循环体里删除或新增列表元素会出现非常诡异的结果。比如你想把列表里所有小于 5 的元素删除numbers [1, 3, 5, 7, 2, 4] for num in numbers: if num 5: numbers.remove(num) print(numbers)你会发现结果不是[5, 7]而是[3, 5, 7, 4]。原因在于for循环是按“当前索引”依次拿元素的当你删除一个元素后后面所有元素都会向左移动一位导致下一个原本该被处理的元素被跳过。所以无论如何都不应该在遍历列表的同时直接修改它的长度。这种场景的正确做法通常有两种。一种是在副本上遍历for num in numbers[:]: # 遍历的是原列表的拷贝 if num 5: numbers.remove(num)另一种是先把要保留的元素收集到新列表里numbers [num for num in numbers if num 5]后面这种列表推导式写法最简洁也是 Python 社区更推荐的方式。记住一条原则不要在遍历一个容器时增删它的元素。如果非要在遍历过程中修改那就先拷贝一份作为遍历对象。5.3 条件判断优先级混乱写条件表达式的时候还有一类非常隐蔽的坑就是运算符优先级。Python 里and的优先级高于or这意味着表达式a or b and c会先计算b and c再和a做or运算这往往和人的直觉不一样。来看这个例子sale True stock False member True if sale or member and stock: print(可以下单)你可能会以为这个表达式是“sale或member同时成立且stock有库存”但按照优先级规则它实际是“sale成立或者member且stock同时成立”。由于 Python 先算member and stock结果是False再算sale or False结果是True所以会输出“可以下单”。这个坑的解决办法就是当你觉得逻辑可能让人产生疑惑时直接加括号。(sale or member) and stock和sale or (member and stock)完全是两个意思加了括号之后代码的意图一目了然别人和你自己以后看代码时都不用猜。不要觉得加括号很菜写出让看代码的人不会理解错的分支比写出“看起来很简洁”的代码重要得多。5.4 循环嵌套与 break 只跳出内层循环另一个高频问题出在嵌套循环和break的配合上。很多新手以为break能把所有层级的循环都退出但实际上break只作用于它所在的最内层循环。举个例子for i in range(3): for j in range(3): if j 1: break print(i, j)这段代码里内层循环在j 1时跳出但外层循环还是会继续到下一个i。输出结果会是0 0、1 0、2 0而不是只输出一次。如果你确实想“跳出所有嵌套循环”常见有三个办法。第一个是用一个标志位在每个循环里都判断这个标志位一旦需要退出就设置为True然后层层break。这个方案有点啰嗦但很直观。第二个是把这个双层循环封装成一个函数满足退出条件时直接用return返回这通常是最干净的方案。第三个是 Python 3.8 之后你可以使用海象运算符在循环条件里判断但可读性较差不推荐。我的建议是逻辑比较复杂的嵌套循环优先考虑拆成函数加return的方式因为流程控制的复杂度一旦变高靠break和标志位在多层循环里跳来跳去很容易让代码变得很难读。6. 最后分享几点实操体会流程控制这一块的内容说到底就是“条件分支”和“循环”这两条主干加上中断、跳过、默认分支这些细节。学完这篇之后我建议你别急着往后翻先用 4.2 节的猜数字游戏练手再自己改造一下比如加入“猜错 5 次提示答案”或者“记录连续猜中的次数”。写代码这个事“看懂了”和“会写了”之间隔着一道很宽的鸿沟只有亲手敲过、报过错、排查过逻辑问题这道鸿沟才会慢慢变窄。我个人在实际项目中还有一个体会遇到逻辑复杂的地方多用小函数把流程控制片段拆出去而不是在一个函数里堆一长串if/elif和for/while。一个函数里嵌套超过两层循环或者出现三个以上的条件分支代码的可读性就会急剧下降。这时候把它拆成独立的小函数每个函数只负责一个环节阅读和调试都会轻松很多。另外一个调试小技巧是当你发现流程控制跑出来的结果和预期不一致时不要盯着代码反复看。在关键的分支和循环里加上print把变量的当前值和走到哪个分支打印出来程序一跑逻辑问题基本当场暴露。这种“打印大法”虽然原始但在处理流程控制相关的问题时真的很好用尤其是在项目早期、日志体系还没搭起来的时候它比任何复杂调试工具都省时省力。