
刚帮一个刚入门 Python 的朋友排查了一段代码问题很简单——他用if判断用户输入时写成了if 1 age 18逻辑上完全没错但在实际业务里年龄小于 0 或者大于 120 的数据他却没有处理。其实这不算 Bug而是典型的“流程控制没有设计完整”。很多初学者学 Python最先接触的就是变量、列表、字典然后直接跳到函数和类反而把if、for、while这些最基础的东西当作“顺便看两眼”。等到真正写爬虫、写自动化脚本、写量化策略的时候才发现自己写的代码要么逻辑混乱、要么死循环、要么分支条件永远走不到。这篇文章就是要把 Python 的流程控制彻底讲透。我会结合真实业务场景把条件分支、循环、循环控制、以及 3.10 新增的结构模式匹配全部拆开来讲。适合刚入门想夯实基础的人也适合写了段时间但对自己代码逻辑不满意的人。看完之后你至少能把“判断条件怎么组合、循环什么时候用for什么时候用while、怎么优雅地终止循环”这些问题理清楚。1. 流程控制到底是什么1.1 程序为什么需要流程控制先想一个问题你现在写一个脚本从上到下执行完能做的事非常有限。比如读取一个配置文件如果文件不存在程序就直接崩溃了如果存在但格式不对你又得换一种解析方式。这时候程序就必须“根据情况走不同的路”。流程控制通俗讲就是控制代码在什么条件下执行、重复执行多少次、以及什么时候跳过或跳出。它是代码的“骨架”数据是“血肉”。骨架歪了数据再漂亮也没用。Python 的流程控制分三大类顺序结构默认逐行执行这是基础条件分支用if / elif / else根据条件决定走哪条分支循环结构用for或while重复执行一段代码。这里要先纠正一个误解很多人觉得流程控制就是if和for其实break、continue、pass、else子句、异常处理中的try/except也算流程控制的“辅助控制语句”。它们单独出现时作用不大但组合起来才能写出真正健壮的代码。1.2 控制流与缩进的关系Python 和其他语言很大的一个区别是没有大括号靠缩进表示代码块。age 20 if age 18: print(你已经成年) print(这段也是if里的) print(这段和if无关)很多人踩过这个坑看起来缩进一样实际上有的是空格、有的是 Tab或者缩进层级不对直接报IndentationError。我的建议是统一用 4 个空格不要用 Tab。编辑器里把 Tab 自动展开成空格可以避免一堆莫名其妙的问题。另外if、for、while后面的冒号非常关键漏了就是语法错误。这个错误很常见尤其写习惯了 JS 或 Java 的人很容易顺手写成分号或漏掉冒号。2. 条件分支if / elif / else 的选择逻辑2.1 基本语法与条件的本质if的语法很简单但很多人搞不清楚“条件”到底能写什么。if 条件表达式: 代码块注意Python 里“条件”不一定是布尔值。任何对象都可以直接放在if后面它会走一遍bool()转换。常见的规则是数字 0 为False非 0 为True空字符串、空列表[]、空字典{}、空集合set()为FalseNone永远为False其他情况基本为True。这个特性非常实用写判断的时候可以很简洁# 不推荐的写法 if len(user_list) 0: print(有用户数据) # 推荐的写法 if user_list: print(有用户数据)但也要注意太依赖真值判断会影响可读性。比如判断“用户是否已登录”用if user is not None会比if user语义更清晰。这里没有绝对的对错主要看团队风格和上下文。2.2 if / elif / else 的执行顺序if / elif / else的执行逻辑是从上往下依次判断一旦某个条件满足后面的分支就不会再执行。这是很多人写代码时容易忽略的一点。score 75 if score 60: result 及格 elif score 80: result 优秀 else: result 不及格这段代码看起来没毛病但实际上是错的。因为当score是 85 的时候先走了score 60这个分支直接返回“及格”后面的elif永远不会被检测。所以条件的顺序非常关键必须把范围更大的判断往后放或者反过来把更严格的条件往前放正确版本应该是score 85 if score 80: result 优秀 elif score 60: result 及格 else: result 不及格这个例子特别典型。我在带新人的时候经常能看到这种逻辑顺序搞反导致的隐蔽 Bug。排查方法也很简单把所有边界值挨个跑一遍60、79、80、85看看输出是否符合预期。2.3 三元表达式单条件快速判断如果分支逻辑非常简单比如只在两个值里选一个可以用三元表达式status 成年 if age 18 else 未成年这个写法等价于if age 18: status 成年 else: status 未成年三元表达式的优点是简洁但嵌套多了以后可读性急剧下降。我见过有人写这种代码result A if x 0 else B if x 0 else C这种链式三元表达式不是不能用但最好少用。如果你觉得逻辑复杂老老实实写if / elif / else反而更容易维护。代码是写给人看的不是写给自己炫技的。3. 循环结构for 与 while 的选择与进阶3.1 for 循环遍历一切可迭代对象Python 的for循环和其他语言的for(int i0; in; i)完全不一样。它是基于迭代器协议的直接遍历一个可迭代对象。# 遍历列表 for fruit in [苹果, 香蕉, 橙子]: print(fruit) # 遍历字符串 for char in hello: print(char) # 遍历字典的键 user {name: 张三, age: 25} for key in user: print(key, user[key]) # 遍历字典的键值对 for key, value in user.items(): print(key, value)新手最容易疑惑的一个问题是在循环里修改正在遍历的列表会不会出问题我直接说结论会。当你遍历一个列表的同时删除元素会导致元素跳过或索引错位。nums [1, 2, 3, 4, 5] for num in nums: if num % 2 0: nums.remove(num) print(nums) # 结果不是 [1, 3, 5]而是 [1, 3, 5]还是 [1, 3]?实际跑一下会发现结果不可预测因为删除元素后列表索引变了循环内部却仍然按原计划取下一个索引。正确的做法是遍历副本或者直接用一个新列表收集结果nums [1, 2, 3, 4, 5] nums [num for num in nums if num % 2 ! 0]这是列表推导式的经典应用后面还会细讲。3.2 while 循环条件满足就继续while循环的逻辑更直观——只要条件为真就一直执行。它和for最大的区别在于循环次数可能未知。什么时候用while最典型的场景是用户输入校验直到用户输入合法数据才退出轮询任务比如不断检查某个队列是否有新任务游戏主循环比如处理玩家动作直到退出游戏。while True: user_input input(请输入一个正整数) if user_input.isdigit() and int(user_input) 0: break print(输入不合法请重新输入)这段代码里用了while True搭配break这也是实际业务中最常用的模式。很多人写 while 有一个致命问题忘记更新循环条件导致死循环。count 0 while count 10: print(count) # 如果没有下面这一行程序永远停不下来 count 1死循环不一定是坏事有些场景我们需要故意让程序一直跑比如事件监听但凡是业务逻辑里的计数循环一定记得更新条件。3.3 for 与 while 的区分记忆我见过不少初学者纠结“到底该用for还是while”。其实有个简单粗暴的判断标准如果你知道要循环多少次或者遍历对象里的每一个元素用for如果你不确定循环次数只确定停止条件用while。用for去写一个不确定次数的循环会比较别扭用while去遍历一个列表则需要自己管理索引代码反而冗长。大多数场景优先选for因为它的边界处理更安全不容易漏掉条件更新。4. 循环控制实战break、continue、pass 与 else4.1 break提前终止循环break是流程控制里最常用的关键词作用是立即跳出当前整个循环。注意是跳出循环不是跳出代码块。最常见的场景是“找到目标就停”。比如在一个列表里查找第一个大于 100 的数numbers [45, 89, 120, 67, 300] for num in numbers: if num 100: print(f找到第一个大于100的数: {num}) break这个逻辑如果你不用break就会把列表全部遍历完浪费不必要的性能。尤其当你处理的是大文件或大规模数据时及时break能节省大量时间。break是只能跳出所在的那一层循环。如果你在嵌套循环内层写break外层循环还会继续跑。这个细节很多人栽过跟头for i in range(3): for j in range(3): if j 2: break print(i, j) print(f---外层循环第 {i} 轮结束---)如果想让break直接跳出所有嵌套循环常见做法是加一个标志位或者把嵌套循环封装到一个函数里用return跳出。4.2 continue跳过这一次循环continue和break很容易混淆。它不结束整个循环只是跳过当前这一次迭代直接进入下一次。比如打印 1 到 10 之间的偶数for num in range(1, 11): if num % 2 ! 0: continue print(num)continue特别适合“过滤掉不需要处理的数据”的场景。比如处理日志文件时跳过空行和注释行for line in lines: line line.strip() if not line or line.startswith(#): continue # 处理有效日志行 parse(line)这里体现了 Python 真值判断的一个实用点not line能直接判定空字符串不需要再写len(line) 0。4.3 pass占位符什么都不干pass和break、continue不同它是空语句什么都不做只是语法占位。什么时候需要比如你写代码时先搭好框架具体实现后面再补def send_email(user): pass # TODO: 接入邮件服务 def process_data(data): pass # TODO: 完成数据分析逻辑这种情况如果不用pass函数体为空Python 直接报语法错误。还有在自定义异常类时也常用class UserNotFoundError(Exception): pass注意pass不能和break、continue混用。break和continue是流程控制pass只是“这里什么都没写但要占个位置”。4.4 循环的 else 子句一个经常被忽略的宝藏这是我必须特别强调的。很多 Python 教程都不讲循环里的else但它特别好用。for和while后面都可以跟一个else块它的执行时机是循环正常结束时执行如果循环被break中断则不执行。最经典的案例是搜索场景target 7 numbers [3, 5, 7, 9] for num in numbers: if num target: print(找到了) break else: print(没有找到)这段代码的逻辑等价于“搜索成功退出失败则走 else”。如果你不用else就得手动加一个标志位found False for num in numbers: if num target: found True break if not found: print(没有找到)两种写法都能达到目的但带else的明显更简洁。我在很多老代码里见过各种手写标志位的写法实际上 Python 已经给了更好的方案。5. 推导式用一行代码替代循环5.1 列表推导式的基础用法推导式不是流程控制的“必需品”但是写 Python 必须会的东西。它的本质是用一行表达式完成循环收集结果的操作。先看一个最基础的例子。把 1 到 10 的平方收集到一个列表里# 普通写法 squares [] for i in range(1, 11): squares.append(i ** 2) # 列表推导式 squares [i ** 2 for i in range(1, 11)]两行变成一行这就是推导式最直观的价值。而且推导式不只是简单它在性能上通常优于普通 for 循环 append因为 Python 对推导式做了底层优化。实际开发中推导式最常见的场景是过滤和转换数据。比如从一个订单列表里取出所有金额大于 100 的订单 IDorder_ids [order[id] for order in orders if order[amount] 100]5.2 字典推导式与集合推导式列表推导式是入门但生产环境里字典推导式同样常用。# 生成一个数字和它的平方组成的字典 squares_dict {x: x ** 2 for x in range(1, 6)} # 结果: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25} # 从一个列表中提取数据构建字典 user_ids {user[name]: user[id] for user in users}集合推导式则适合去重场景# 求列表里有哪些不同的首字母 words [apple, banana, cherry, avocado] first_letters {word[0] for word in words} # 结果: {a, b, c}5.3 推导式中的 if 条件推导式支持嵌套if条件但是要分清楚两种位置# 过滤模式: 先循环后判断 even_squares [x ** 2 for x in range(1, 11) if x % 2 0] # 三模式: 在表达式位置做条件判断 result [x if x 5 else 0 for x in range(1, 11)]第一种是“满足条件才收集”第二种是“不管怎样都收集但收集的内容不同”。这两种写法混了会直接出 Bug初学者要特别注意。5.4 生成器表达式省内存的推导式如果把列表推导式的中括号换成小括号就变成了生成器表达式它不会一次性生成所有数据而是惰性计算适合处理大数据集。# 列表推导式一次性创建全部数据 squares_list [x ** 2 for x in range(1000000)] # 生成器表达式迭代时才计算 squares_gen (x ** 2 for x in range(1000000))两者消费方式也不同列表可以反复遍历生成器只能遍历一次。实际处理大文件时生成器表达式能在很大程度上缓解内存压力。6. 流程控制常见坑位与排查技巧6.1 条件判断里最容易犯的几个错第一个坑是if里的比较条件写错。最常见的是把写成Python 会报语法错误这个倒还好编译器能查出来。怕的是逻辑上没报错但结果不对# 这个条件永远不会为 True if admin admin: # 语法错误无法运行 pass # 这个条件永远成立因为非空字符串 True if admin: print(永远会打印)第二个坑是浮点数比较。很多人写代码判断金额if total_amount 0.1: print(等于0.1)但0.1 0.2 0.3在 Python 里是False因为浮点数二进制存储有精度误差。正确写法是if abs(total_amount - 0.1) 1e-9: print(约等于0.1)或者用decimal模块处理金额。这个坑在做量化交易、数据计算时尤其害人。第三个坑是逻辑运算符的使用。Python 里and、or、not和很多语言一样但短路径的优先级容易搞混。比如# 这个表达式的结果是 False if not condition1 or condition2: pass它解析成(not condition1) or condition2而不是not (condition1 or condition2)。如果没搞清楚优先级业务逻辑就会整个翻转。6.2 循环中的常见坑死循环、索引越界、无限添加死循环的排查方法很简单直接加一个打印看循环条件是否变化。但有些死循环比较隐蔽。比如用while循环处理队列数据时总是往队列尾部添加新任务但处理速度跟不上添加速度queue [1, 2, 3] while queue: task queue.pop(0) # 产生新任务 queue.append(task 1)这样可能永远处理不完。排查思路是给循环加一个最大迭代次数限制max_loop 1000 count 0 while queue and count max_loop: count 1 task queue.pop(0)还有一个常见问题是用range遍历时索引越界。比如nums [1, 2, 3, 4, 5] for i in range(len(nums)): if nums[i] % 2 0: nums.pop(i)这里在循环体内删除元素导致索引越界或跳过元素。这个问题在前面也提过遍历时不要修改原列表结构要么遍历副本要么用推导式重建列表。6.3 排查流程控制问题的实战思路我现在排查流程控制相关的 Bug基本会按这个顺序先看缩进层级确认哪些代码真的在哪些分支里边界值逐个试比如if age 18把 17、18、19 都跑一遍在关键位置插入打印确认程序到底走进了哪个分支用调试器断点VSCode 或 PyCharm 里打断点看变量的实时变化简化复现删掉无关代码只保留最小复现逻辑。很多时候问题不是出在流程控制的语法上而是出在设计的思路混乱。比如一个业务逻辑需要 5 种不同结果的判断硬写 5 个if不如先抽象出一个数据字典来映射结果。7. match case3.10 时代的新选择7.1 match 的基本用法Python 3.10 引入了match语句类似于其他语言的switch但功能更强。它的基本形态是match command: case start: print(启动) case stop: print(停止) case _: print(未知命令)这里的_是通配符匹配所有未定义的情况相当于else。7.2 match 不只是值匹配那么简单match最强大的地方是结构匹配它可以解包数据结构def handle_point(point): 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})) case _: print(不是二维坐标)这种写法在处理 API 返回的不同数据结构时特别有用。比如一个接口可能返回不同格式的错误信息用match去解包会非常清晰match response: case {status: success, data: data}: process(data) case {status: error, code: code, message: msg}: raise RequestError(code, msg) case _: raise UnknownResponseError(response)match能帮你省去一堆if嵌套判断代码整体更扁平、更易读。不过要注意match不会自动 fall-through即匹配成功一个 case 后不会继续执行下一个这和 C 语言里的switch不同也不用额外写break。7.3 什么时候不要用 match虽然match很优雅但它只适用于 Python 3.10很多项目还跑在 3.8 或 3.9 上使用前要先确认环境。另外match适合结构化的匹配场景如果只是判断一个值是否大于某个数字老老实实用if就好。我在实际项目中只有在处理协议解析、接口返回分型等场景下才用match普通业务逻辑还是用if更直白。8. 流程控制与实战场景的结合8.1 爬虫中的流程控制写爬虫是 Python 入门最常见的应用方向。爬虫里流程控制遍布各处从请求、解析到数据清洗都离不开。比如爬取一个需要登录的网站需要先检查是否有登录态def fetch_page(session, url): response session.get(url) if response.status_code 200: # 检查是否跳转到登录页 if response.url.endswith(/login): login(session) response session.get(url) return response elif response.status_code 404: print(f页面不存在: {url}) return None else: response.raise_for_status()这个逻辑里用了两层if / else再加上异常处理就是一个比较稳健的请求函数。用循环控制一次抓取多页时for循环加上合理的time.sleep()就能避免被封。8.2 数据清洗中的流程控制做数据分析时经常需要对原始数据做清洗。流程控制主要用在条件筛选上。比如一个用户行为日志列表需要过滤掉无效数据并分类def clean_user_logs(logs): valid_users [] invalid_logs [] for log in logs: if not log.get(user_id): invalid_logs.append(log) continue if log[action] click and log[value] 0: invalid_logs.append(log) continue valid_users.append(log) return valid_users, invalid_logscontinue在这类场景里能避免 if 嵌套太深。你会发现想清楚“哪些数据要跳过”比“哪些数据要保留”更容易写代码。8.3 量化交易策略中的流程控制量化交易策略的代码是流程控制的集大成场景。策略信号生成、订单管理、风控判断每一步都是条件分支和循环的组合。一个简单的双均线策略判断买卖信号def generate_signal(prices, short_window, long_window): if len(prices) long_window: return hold short_ma sum(prices[-short_window:]) / short_window long_ma sum(prices[-long_window:]) / long_window if short_ma long_ma: return buy elif short_ma long_ma: return sell else: return hold这里的流程控制虽然简单但从策略回测到实盘中间还有大量循环遍历历史数据、逐笔判断交易的环节。流程控制不熟策略代码写出来基本跑不通。8.4 风控系统的流程控制风控系统里最典型的就是规则引擎。多条规则按顺序执行命中即中断def risk_check(order): if order[amount] 10000: return high_risk if order[user_risk_score] 80: return high_risk if order[is_fraud_frequency]: return high_risk if order[country] in black_country_list: return medium_risk return low_risk这种逐层 if 判断的写法本质上就是一个优先考虑强规则的流程控制设计。顺序很重要一定把最致命的情况放在最前面。9. 流程控制的性能与代码风格建议9.1 循环性能优化要关注的点流程控制写对了接下来就是效率问题。Python 循环本身比 C 语言慢但有几个优化思路第一尽量用推导式替代显式循环。就像前面说的推导式在底层有优化代码也更简洁。第二避免在循环体内做重复计算。比如# 不推荐 for i in range(len(items)): total sum(items) # 每次循环都求一遍和 print(items[i], total) # 推荐 total sum(items) for i in range(len(items)): print(items[i], total)第三合理使用 break 和 return 缩短执行时间。查找类任务第一次命中就退出能减少不必要的计算。第四大循环里尽量用局部变量和函数调用。Python 的全局变量查找比局部变量慢。比如range循环里频繁访问模块级函数性能会有可见差异这是 Python 的 LEGB 作用域规则决定的。9.2 流程控制的代码风格规范流程控制写多了代码风格会直接影响维护成本。以下是我个人比较坚持的几个习惯条件表达式里先写更具体的判断把大多数情况放后面避免过深的嵌套超过三层缩进就要想办法抽函数或者反转判断条件用continue提前返回区分is和判断None用is None判断值相等用循环里用enumerate而不是range(len(...))既清晰又能同时取到索引和值# 不推荐 for i in range(len(names)): print(i, names[i]) # 推荐 for i, name in enumerate(names): print(i, name)使用zip并行遍历多个列表避免用索引去对接多个列表。9.3 代码可读性流程控制的隐形质量指标我审别人的代码时第一眼看的就是流程控制。一个函数的嵌套层级、循环长度、分支复杂度直接决定这个函数能不能被快速理解。如果一段流程控制的代码需要读两遍才能看懂那它就算写对了也是“坏味道”。推荐一个小技巧把“正常流程”放在前面把“处理异常”的if判断放在后面或者反过来用提前返回。# 嵌套写法 def process_order(order): if order is not None: if order[paid]: ship(order) else: cancel(order) else: raise ValueError(订单不能为空) # 提前返回 def process_order(order): if order is None: raise ValueError(订单不能为空) if order[paid]: ship(order) else: cancel(order)第二种写法清晰很多。它把异常条件和正常逻辑剥离开代码阅读者是顺着主路径往下看的不会被一堆分支干扰。这就是流程控制设计上的一个核心原则让正常路径尽量扁平。10. 一套完整的流程控制示例模拟用户登录系统为了把上面的知识点串起来我写一个完整的示例模拟一个简单的登录系统。这个场景会用到if判断、while循环、break、continue、字典数据结构和函数封装。users_db { admin: {password: 123456, status: active}, guest: {password: guest123, status: inactive}, } def validate_login(username: str, password: str) - bool: user users_db.get(username) if not user: print(用户不存在) return False if user[status] ! active: print(用户已被禁用) return False if user[password] ! password: print(密码错误) return False return True def login(): max_attempts 3 attempts 0 while attempts max_attempts: username input(请输入用户名: ).strip() password input(请输入密码: ).strip() if not username or not password: print(用户名和密码不能为空请重新输入) continue attempts 1 if validate_login(username, password): print(登录成功) return remaining max_attempts - attempts print(f剩余尝试次数: {remaining}) print(连续尝试失败账户已锁定)这段代码里有一些值得注意的细节validate_login用提前返回把判断逻辑分成了三个互相独立的点while循环用attempts计数实现最多三次尝试continue跳过空输入不消耗尝试次数登录成功用return直接退出整个函数而不是靠break跳出循环再判断。这个示例的流程控制设计思路和我前面讲的“正常路径扁平化”“边界条件前置”是完全一致的。你可以在自己机器上跑一下输入各种非法值感受一下。我个人在实际操作中的体会是流程控制学得好不好不看你背不背得出来if和for的语法而看你设计代码时能不能想清楚“每一步该往哪走”“什么条件下停下来”。很多新手写的代码看起来语法都对但逻辑黑洞一大堆本质上就是流程控制没练够。最后再分享一个小技巧当你觉得流程控制特别混乱的时候不要急着改代码先在纸上画一个简单的流程图把“入口”“判断点”“终点”标出来然后照着图翻译成 Python。随着经验积累你会发现画图的次数越来越少代码也越来越干净。脚踏实地把流程控制吃透后面学函数、装饰器、并发编程都能顺很多。