写Python写了几年之后,我越来越觉得,判断和循环这四五个关键字,才是程序真正“活”起来的地方。你想想,一个脚本从头到尾没有if、没有for,那它基本就是一台顺序播放的复读机;只有当你写出if score >= 60的那一刻,程序才开始像人一样做选择,也只有当你写出for student in class_list的时候,它才学会不厌其烦地重复劳动。这篇文章就把分支和循环彻底讲透:怎么判断、怎么遍历、怎么中途退出,哪些写法是坑,哪些组合是套路,以及怎么用它们拼出真正能解决实际问题的代码。不管你是刚学Python没几天的新手,还是写了一阵子想补基础的老同学,这篇文章都适合你慢慢看。
1. 分支语句:让程序拥有选择的资格
1.1 缩进不是摆设,它是Python划分代码块的唯一依据
很多从其他语言转过来的朋友,第一次写Python最不适应的就是缩进。别小看这个细节,在大多数语言里,代码块靠花括号{}界定,缩进纯粹是给你自己看的;但在Python里,缩进就是语法本身。if下面跟着的、缩进相同的几行代码,会被当成一个整体,要么全部执行,要么一个都不执行。
score = 85 if score >= 60: print("及格了") print("下节课继续加油") print("这行不管考多少分都会打印")你看上面这段,print("及格了")和print("下节课继续加油")缩进一致,它们是一组;而最后一行没有缩进,它不属于if管辖。这种设计的初衷其实很实在:强制所有程序员都用统一的缩进风格,代码天然就整齐,不会出现“花括号写错位置”这种问题。代价就是你写的时候必须小心,Tab和空格不能混着用,否则解释器会报IndentationError,看起来像玄学,其实就是缩进不统一。
我自己在带新人时经常说,判断分支的本质就是“翻译你的想法”。你想表达“如果成绩及格就提示,否则就提醒”,那就顺着思路写:
if score >= 60: status = "及格" else: status = "不及格"1.2 elif不是“else if”那么简单,它帮你省掉一层嵌套
很多语言里你要写“多重条件”,得这样:
- 先写一个
if - 在
else的代码块里再套一个if
Python专门给出了elif,让多分支判断在同一个平面展开,而不是一层套一层。比如把成绩分等级:
score = 87 if score >= 90: level = "优秀" elif score >= 80: level = "良好" elif score >= 70: level = "中等" elif score >= 60: level = "及格" else: level = "不及格"请注意执行逻辑:它从上到下挨个判断,一旦某一个条件成立,执行完对应代码块之后,剩下的elif和else统统跳过,不会再继续判断。也就是说,如果你第一行写了if score >= 60,下面又写了elif score >= 80,那分数85的人走完第一个分支就结束了,根本到不了第二个分支。这就是很多新手写出“逻辑好像反了”的问题的根源——条件顺序本身就是逻辑的一部分。
1.3 条件判断的关键不在于比大小,而在于“这个值是否为真”
你可能会觉得“条件就是比较大小嘛”,其实不是。Python里if后面跟的本质上是一个“布尔值”,也就是真(True)或假(False)。当你写if score >= 60,Python先算出score >= 60这个表达式的结果是True还是False,再决定走哪条路。
这个概念在实战中会衍生出很多你没注意到的细节。比如,Python里任何对象都能直接拿来当条件判断:
name = "" if name: print("名字非空") else: print("名字是空的")你猜输出什么?是“名字是空的”。因为在Python的隐式规则里,下面这些值统统会被当作False:
- 数字
0、0.0 - 空字符串
"" - 空列表
[]、空元组()、空字典{}、空集合set() None
反过来,任何“非空”的东西,比如数字1、字符串" "(注意,里面有空格也算非空)、非空列表[0],都是True。
这个特性用对了非常爽。比如判断一个列表是否有元素,你不需要写if len(items) > 0,直接写if items:就行。但坑也很隐蔽——如果你本意是想判断“某个变量是否为None”,结果那个变量的值恰好是0或者空字符串,那if variable:就会走False分支,判断就错了。所以记住这个口诀:判断“有没有”用隐式布尔,判断“是不是None”必须显式写is not None。
还有一个跟新手聊得最多的点:==和is的区别。==比的是“值相等”,is比的是“是不是同一个对象”。日常判断值用==就够了,判断是否是None用is None,这是Python社区的惯例。别混用,否则遇到大整数或长字符串比较时会产生难以解释的诡异结果。
2. 循环语句:把重复动作交给机器
2.1 for循环的真正面目:遍历可迭代对象,而不是“循环N次”
很多初学教材会说“for循环用来循环N次”,这个说法其实漏掉了重点。for在Python里核心是遍历——从头到尾访问一个“可迭代对象”里的每一个元素。这个可迭代对象可以是列表、字符串、元组、字典、文件的行,甚至是你自定义的生成器。
students = ["小明", "小红", "小刚"] for name in students: print(f"正在处理: {name}") word = "Python" for ch in word: print(ch) # 逐个字符输出这是for循环和C语言等for (i=0; i<n; i++)最大的不同:Python把“怎么从序列里取下一个元素”这个脏活累活封装成了迭代器,你只需要关心“拿到每个元素之后干什么”。
遍历字典时,小心别犯低级错误。直接for key in user_info:遍历的是键;想拿值要for key in user_info: print(user_info[key]);更推荐的做法是:
user_info = {"name": "张三", "age": 18, "city": "北京"} # 同时拿到键和值 for key, value in user_info.items(): print(f"{key}: {value}")这是Python 3里最常用的字典遍历方式,不用再像更早版本那样麻烦。
2.2 while循环:先判断,后执行,条件满足就一直跑
while和for最大的区别就是:for知道自己要遍历什么,遍历完就停;while只认一个条件公式,条件为True就一直跑下去,直到条件变False或者被强制打断。
count = 1 while count <= 5: print(f"第{count}次循环") count += 1上面这段,你要是忘了count += 1这一行,count永远不会变,条件永远是True——好了,这就是经典的死循环。新手遇到死循环第一反应是按Ctrl+C中断程序,但写代码时要养成习惯:每写一个while,先问自己“这个循环靠什么停下来”。
while最典型的实战用法是“不知道要循环多少次,但知道什么时候该停”。比如写一个猜数字游戏,你没法预先规定用户猜几次,只能靠条件判断:
answer = 42 while True: guess = int(input("猜一个数字: ")) if guess == answer: print("猜对了") break elif guess < answer: print("再大一点") else: print("再小一点")这里while True从字面上看是“永远循环”,但循环体里藏着break,一旦猜对就跳出去。这种写法叫“哨兵模式”,在菜单交互、消息处理、服务器监听里非常常见。
2.3 range()的两个边界细节:左闭右开,步长默认1
for循环配合range()是“循环固定次数”的主力。但range(5)到底生成0到4还是0到5,这是新手最容易记混的。你只需要记住一句话:range包左不包右,也就是range(start, stop)包含start,但不包含stop。
# 生成 0, 1, 2, 3, 4 for i in range(5): print(i) # 生成 2, 4, 6 for i in range(2, 8, 2): print(i)range(2, 8, 2)里的2是步长,它会输出2、4、6,到8就停了,因为8不被包含。想做倒序循环,步长设成负数:
for i in range(5, 0, -1): print(i) # 输出 5 4 3 2 1步长边界这种细节,实地测试时踩过坑的人一定有共鸣。我接手过一段统计代码,同事写的range(1, len(arr)),他自己以为会遍历到数组末尾,结果最后一个元素被漏掉了,查了半小时才发现是右边界“少了一个”。所以当你发现自己循环里“莫名少处理了一个元素”,第一反应就检查range边界。
2.4 想要序号又想要值?用enumerate别自己数数
遍历列表时经常需要同时知道“当前元素”和“它的位置”。新手做法往往是:
students = ["小明", "小红", "小刚"] for i in range(len(students)): print(i, students[i])这种写法没错,但不够优雅,而且每次都要写len()和下标访问,繁琐。Python专门提供了enumerate:
for index, name in enumerate(students): print(index, name) # 想从1开始编号也可以 for index, name in enumerate(students, start=1): print(index, name)我强烈建议你养成用enumerate的习惯,它读起来更像人话:“遍历students里的每个元素,顺便带上它的序号”。这种语法糖虽然不是“分支和循环”的必要知识,但能让你写出来的循环一下子有老手的味道。
3. 循环控制:break、continue、else各自的分工
3.1 break:找到目标立刻撤退,别做多余的劳动
循环里如果已经达成了目标,就没必要继续遍历下去了。比如你要判断列表里是否存在某个数字,找到就退出:
numbers = [3, 7, 2, 9, 5] target = 9 for num in numbers: if num == target: print(f"找到了: {num}") break如果没有break,程序还要继续比较后面的5;有了break,循环在num == target的瞬间就中断了。数据量小的时候无所谓,但如果列表有几百万条,break能帮你省掉大量无用功。更重要的是,它让你的代码意图更清楚:“我找的是第一个满足条件的,找到了就走”。
3.2 continue:跳过这一次,但保留整体循环
continue和break看着像兄弟,干的活完全不同。continue不会结束循环,它只是“跳过本轮剩余代码,直接进入下一轮”。最常见的场景是过滤脏数据:
scores = [85, -1, 90, 76, -1, 59] for score in scores: if score < 0: continue if score >= 60: print("及格:", score) else: print("不及格:", score)这里score为-1时,continue会跳过下面两个if,直接进入下一轮。没有continue,你就得把后面的判断全部套进if score >= 0:里,嵌套层数变深,可读性下降。这就是continue的核心价值:用一个小跳转,换掉一层缩进。
3.3 for-else:循环“正常跑完”才执行的隐藏分支
这是Python里很多人学了几年都没用过的特性。for循环后面可以跟一个else语句块,但它的语义很特殊:循环没有被break打断、正常遍历完所有元素之后,才会执行else里的代码;如果循环是被break跳出来的,else不会执行。
听上去有点反直觉,但它解决了一个很经典的场景——查找并判断是否存在:
numbers = [3, 7, 2, 9, 5] target = 8 for num in numbers: if num == target: print(f"找到了: {num}") break else: print("没找到")运行上面这段,因为列表里没有8,循环正常跑完,else分支执行,打印“没找到”。这个特性比“设置一个find_flag变量,循环结束后再判断flag”要简洁得多。我见过很多人在这个场景里写这样的代码:
found = False for num in numbers: if num == target: found = True break if found: print("找到了") else: print("没找到")不能说错,但for-else可以把这个逻辑压缩得更紧凑。唯一要提醒的是,while也有else子句,语义完全一样。不过这个语法毕竟少见,建议在团队里使用前先提醒大家,否则有人会以为else是“循环结束后的统一收尾”,导致理解偏差。
3.4 嵌套循环里break的陷阱:它只跳出一层
写两层以上循环时经常有一个需求:找到目标后,里外两层循环都想停。但你要是天真地写一个break,会发现它只跳出最内层的循环,外层循环依然继续跑。
matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], ] target = 5 found = False for row in matrix: for cell in row: if cell == target: print("找到了") found = True break # 只跳出内层循环 if found: break # 需要第二层break才能跳出外层处理这个问题常见有三种思路:一是用上面这种“标志变量”法,找到后置flag,在外层循环开头再判断一次;二是把查找逻辑抽成一个函数,内部两层循环用return直接结束整个函数;三是用异常处理强行跳出。从可读性角度看,最推荐的是抽函数,因为return的语义最直白:
def find_in_matrix(matrix, target): for row in matrix: for cell in row: if cell == target: return True return False嵌套循环本身就是可读性的风险点,能不用就尽量不用。我在实际项目中经常把嵌套循环替换成any()或itertools.product,目的就是降低复杂度。等你能写出“三层嵌套循环”的时候,也许恰好是应该重构的时候。
4. 分支+循环组合实战:学完语法要会打组合拳
4.1 从需求出发的三个步骤:循环、判断、处理
很多同学学完了if、for、while,却不知道拿到一个实际问题该从哪里下手。我总结了一个适合新手的思考顺序,实战中非常管用——先把需求翻译成一句话:
- 要对什么重复做事情?→ 确定循环对象
- 在重复过程中需要根据什么条件做区分?→ 确定分支条件
- 每次循环里最终要留下什么结果?→ 确定变量如何更新
举一个最经典的例子:统计一个成绩列表里及格人数的占比。
scores = [58, 92, 73, 45, 88, 69, 41, 90] total = len(scores) passed = 0 for score in scores: if score >= 60: passed += 1 percentage = passed / total * 100 print(f"及格人数: {passed}, 及格率: {percentage:.1f}%")你看,整个代码就三步:循环遍历每个分数,在循环里判断是否及格,及格就累加计数器。这个例子看起来简单,但它暗含了循环+分支最常见的组合模式。如果这个结构你吃透了,后面所有复杂逻辑都是在这个基础上加条件、加变量、加嵌套。
4.2 模式一:筛选(Filter),把满足条件的元素挑出来
筛选是循环+分支最基础的应用场景。给你一个列表,你需要挑选出满足特定条件的元素,存到新列表里。
all_numbers = [12, 5, 8, 30, 17, 6, 22, 3] greater_than_ten = [] for num in all_numbers: if num > 10: greater_than_ten.append(num) print(greater_than_ten) # [12, 30, 17, 22]这个模式可以演化出无数变体:筛选非空字符串、筛选重复项、筛选价格超过某个阈值的商品等等。本质上就是“遍历原列表,符合条件就放进空列表”。几乎所有“过滤”“提取”“找出所有……”的需求,都可以套这个框架。
4.3 模式二:计数与累加,用分支决定加不加
筛选是“保留元素”,计数与累加则是“不保留元素,只更新数字”。刚才及格人数就是计数,如果需求变成“计算及格同学的总分”,只需要把passed += 1换成total_score += score:
total_passed_score = 0 passed_count = 0 for score in scores: if score >= 60: passed_count += 1 total_passed_score += score average_passed_score = total_passed_score / passed_count需要注意的是变量初始化。计数器passed_count要从0开始,每命中一次就加1;累加器total_passed_score也要初始化为0,然后每次累加。这类需求很容易写错的地方是:忘记初始化变量,或者把累加写在if外面,导致分数判断逻辑失效。
4.4 模式三:查找(Search),找到即止
查找模式用到break和for-else的组合。它的特点是:你不关心有多少个匹配,你只想知道“有没有匹配”或“匹配的在哪里”。
users = [ {"name": "张三", "age": 20}, {"name": "李四", "age": 35}, {"name": "王五", "age": 28}, ] target_name = "李四" target_user = None for user in users: if user["name"] == target_name: target_user = user break if target_user: print(f"找到了,年龄是 {target_user['age']}") else: print("没有这个用户")用for-else可以更干净:
for user in users: if user["name"] == target_name: print(f"找到了,年龄是 {user['age']}") break else: print("没有这个用户")这种写法在处理“字典列表”“对象列表”时尤其常见,比如从接口返回的一大堆数据里找出指定ID的那一条。很多人一上来就写if target_name in users,但在“列表里每个元素是字典”的情况下,in判断的是“整个字典是否在列表里”,不是“某个键是否等于目标值”,所以必须靠循环配合分支去遍历查找。
4.5 模式四:标志变量,记录“循环过程中发生过什么”
有时候循环结束后,你需要知道整轮遍历中是否出现过某种情况。比如检查一个列表里是否有重复元素:
numbers = [4, 2, 7, 2, 8, 9] has_duplicate = False for i in range(len(numbers)): for j in range(i + 1, len(numbers)): if numbers[i] == numbers[j]: has_duplicate = True break if has_duplicate: break print("有重复项" if has_duplicate else "没有重复项")标志变量has_duplicate的作用是:循环结束后,根据它的值做最终判断。另一种更Pythonic的写法是用集合去重后比较长度:
has_duplicate = len(numbers) != len(set(numbers))不过我这里用嵌套循环是想强调“标志变量”这个思路——它在“检查是否单调递增”“判断所有元素是否合格”“只要有一个出错就标记”等场景非常好用。学会了它,你面对的是“循环内部无法立即得出最终结论”这类问题时,就有了抓手。
4.6 列表推导式:简单循环的优雅压缩版
当循环体里只有一个简单的if分支和一个append操作,Python允许你用列表推导式(List Comprehension)写成一行:
greater_than_ten = [num for num in all_numbers if num > 10]这句等价于之前那四行筛选代码。还有带else的推导式:
labels = ["及格" if score >= 60 else "不及格" for score in scores]推导式的语法结构很有规律:最外层是方括号,里面先写“对元素做的表达式”,再写for来源,最后写可选的if过滤条件。它在处理映射、筛选时效率高、代码短,但要注意一个边界:嵌套的推导式可读性极差,比如那个检查重复项的三层推导式,写到后来连你自己都看不懂。我自己的原则是:一个推导式里只有一个for和一个if,超出这个复杂度就老实写普通循环。
下面这张表把分支循环的常见模式做了个汇总,方便你以后直接“套模板”:
| 需求类型 | 核心结构 | 关键代码骨架 |
|---|---|---|
| 遍历所有元素 | for循环 | for item in iterable: |
| 按条件分流 | if/elif/else | if cond: ... elif cond2: ... else: ... |
| 筛选子集 | for + if + append | result = []加if加append |
| 找目标即停 | for + if + break | 找到后break,可用else处理没找到 |
| 跳过干扰项 | for + continue | 干扰项出现时continue |
| 循环次数固定 | range | for i in range(start, stop, step) |
| 序号+值同时遍历 | enumerate | for idx, val in enumerate(iterable) |
| 简单循环压缩 | 列表推导式 | [expr for item in iterable if cond] |
5. 常见误区与可读性:写出不坑人的分支循环
5.1 嵌套太深的判断,用“提前返回”削平它
分支和循环用多了,最可怕的现象就是“箭头形代码”——一堆if套if,缩进层层加深,看着像一把指向右边的箭头。这种代码维护起来极其痛苦,你改一个条件就得在一堆缩进里翻来翻去。
解决思路是“卫语句”(Guard Clause):把不满足条件的情况提前return或continue,让主线逻辑保持在浅层。
# 不推荐的写法 def process_order(order): if order: if order["status"] == "paid": if order["amount"] > 100: print("VIP订单,优先发货") else: print("普通订单") else: print("未支付,不能发货") else: print("订单为空")# 用卫语句改写的版本 def process_order(order): if not order: print("订单为空") return if order["status"] != "paid": print("未支付,不能发货") return if order["amount"] > 100: print("VIP订单,优先发货") else: print("普通订单")效果完全一样,但第二个版本的“主线”是往下读的,每个异常情况自己处理完就走,不会缠绕在一起。这和continue的哲学是一样的:提前退出比层层嵌套更清晰。
5.2 一长串elif怎么优化:用字典做映射
当你的if...elif...else写满了十来行,而且每个分支做的事情就是“把某个值映射到另一个值”时,就该考虑用字典了。
# 冗长的elif链 def get_weekday_name(day): if day == 1: return "周一" elif day == 2: return "周二" elif day == 3: return "周三" elif day == 4: return "周四" else: return "未知"# 字典映射版 def get_weekday_name(day): weekday_map = { 1: "周一", 2: "周二", 3: "周三", 4: "周四", } return weekday_map.get(day, "未知")字典的get方法还能设置默认值,一举解决了else分支的问题。这种模式在处理状态码转文字、等级转名称、配置项映射时非常管用,比elif链更省代码,而且字典本身就是数据结构,将来想从配置文件中读取也更方便。
如果你的Python版本是3.10及以上,还可以用match语句来写匹配逻辑,但它更适合复杂的模式匹配场景。日常简单的键值映射,用字典就够了,没必要非上match。
5.3 短路求值:and与or能帮你少写一层if
Python里and和or并不一定返回布尔值,它们返回的是“决定结果的那个操作数”。这个特性被称为短路求值。比如:
name = "" default_name = name or "匿名用户" print(default_name) # 匿名用户当name为空字符串(假值)时,or会继续往后看,于是返回了右边的"匿名用户";如果name非空,它就直接返回name本身,后边的值根本不会被评估。这个特性非常适合给变量设置默认值,比写完整的if...else简洁得多。
判断条件时同样可以简化。比如你想表达“用户没登录,或者是管理员,就允许进入”,可以这么写:
if not is_logged_in or is_admin: allow()条件本身也是一门学问。优先级上,not > and > or,比如not a or b其实是(not a) or b。多条件组合很容易写出歧义,我的建议很简单:拿不准优先级就加括号,代码是写给人看的,括号多一点不丢人。
5.4 我调试分支循环的几个土办法
最后分享几个我在实际调试中特别管用的经验。
第一,把变量打出来。很多分支逻辑出错,是因为变量值和你想的不一样。在if判断之前加一行print(score, type(score)),马上就能发现是“字符串类型的'85'被当成数字比较”之类的坑。用f-string打印更清楚。
第二,用边界值测循环。写循环代码,心里要默认拿几种特殊输入去测:空列表、只有一个元素的列表、range(0)、负数步长、全是False的列表。很多隐蔽bug都出在“空列表根本进不了循环”,然后你后续代码用了循环里才赋值的变量,报UnboundLocalError。这种情况特别经典,必须提前预料。
第三,一次只改一个地方。分支循环出问题时,不要同时调整条件和循环范围。先固定循环,单测分支;再固定分支,单测循环。这个习惯能帮你省下大量的排查时间。我在写统计类代码时,甚至会在分支入口处打印一个带标记的信息,比如print("进入及格分支", score),跑一遍下来,数据到底走了哪条路就一览无余了。
聊到这里,我真心觉得分支和循环就是编程的骨架,数据结构和算法反而是长在骨架上的肌肉和器官。骨架歪了,肌肉再发达也是歪的。我自己带项目时有个习惯,先让新人用大白话把需求讲一遍,讲的时候能听到“如果……否则……”“每个……都……”这种表达,我就知道可以用分支循环去实现了。写代码就像是把心里的判断和重复动作翻译成Python方言,翻译多了,自然就形成肌肉记忆。最后再分享一个小技巧:遇到稍微复杂的逻辑,先在本子上写伪代码,别急着敲屏幕。比如“遍历成绩单,分数过60就记一个及格,否则记一个不及格,最后算及格率”这几行大白话,就是最好的设计文档。把它们变成for加if,你离写出靠谱的程序就不远了。