刚帮一个刚入门 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()为False; None永远为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 i=0; i<n; 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 与 else
4.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 的订单 ID:
order_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 case:3.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。随着经验积累,你会发现画图的次数越来越少,代码也越来越干净。脚踏实地把流程控制吃透,后面学函数、装饰器、并发编程都能顺很多。