1. 为什么说条件与循环是所有Python程序的心脏
我经常在带新人和面试的时候问一个问题:抛开框架和第三方库,你自己独立写过最复杂的Python逻辑是什么?结果十有八九的回答里,核心无非就是几层if判断、几个for循环。这恰恰说明了一个事实——条件和循环语句,才是Python程序真正的骨架。你可以不会面向对象、不懂装饰器,但只要把这两块吃透,就已经能解决工作中绝大多数的数据处理、流程控制问题。
很多人觉得条件与循环太基础,恨不得直接跳到爬虫、数据分析。但实际操作中你会发现,报错最多的、调试最久的,往往就是条件判断漏了一个边界值、循环变量写错范围、break和continue用混淆。与其后面反复补课,不如一开始就把这两块庖丁解牛式地拆开。
这篇内容不只是罗列语法,我会把if、elif、else、while、for、break、continue这些关键字背后的运行逻辑、使用场景、易错点全部讲清楚,并且配上可以直接跑通的示例和拆解思路。无论你是刚装好Python准备入门的新手,还是写过一阵子但总觉得控制流不扎实的进阶者,这篇文章都能帮你把地基打牢。
1.1 从“顺序执行”到“控制流”:你写的每行代码都在做选择
先想一个最基础的问题:Python解释器是怎么读你的代码的?答案是自上而下、一行一行执行。这种模式叫“顺序执行”。但现实中的逻辑永远没有这么简单——你要判断用户输入的年龄是否满18岁、判断一个数是否为偶数、判断文件是否存在,然后再决定下一步做什么。这时你就需要“控制流”,而控制流的核心工具就是条件语句和循环语句。
条件语句让程序“分叉”——根据不同的条件走不同的路;循环语句让程序“重复”——在满足条件的情况下反复执行一段代码。两者经常配合使用:循环里嵌条件,条件里触发循环。理解了这个本质,你就能明白为什么每个Python教程都会把这两块放在最前面,因为它们是表达能力的基础单位。
我见过不少人背语法背得很熟,一到写实际功能就卡壳,原因就在于只记住了“语句怎么写”,没想明白“程序怎么走”。所以下面所有内容,我都会带着“程序执行到这里会发生什么”的视角去讲,而不是单纯列格式。
1.2 条件与循环的“最小认知模型”
我自己习惯用一个最简单的类比来理解控制流:把程序想象成一条流水线。
- 顺序执行:零件从传送带一头进来,经过A工位、B工位、C工位,不加干预地流到另一头。
- 条件语句:流水线上装了一个探头,检测到某种特征的零件就拨到左边轨道,否则继续走右边轨道。
- 循环语句:流水线有个回转机构,不合格的零件送回起点重新加工,直到合格才放行。
这套模型虽然简单,但能帮你建立起“程序是在动态运行”的直觉。后面无论你写多复杂的代码——嵌套循环、多层判断、循环里提前退出——本质上都是在给这条流水线增加探头和回转机构。有了这个思维打底,再去看具体语法就不会觉得是一堆死记硬背的关键字。
1.3 一个真实的业务场景拆解
光说不练没有体感,我先给一个贴近日常的例子,后面的知识点都可以套进这个场景里理解。
假设你有一个文件,里面是某班级学生的成绩单,你需要统计:
- 成绩大于等于90分的评为“优秀”
- 60到89分的评为“及格”
- 小于60分的评为“不及格”
- 最后输出每个档位的人数
这个问题用Python解,核心就是:一个for循环遍历所有分数,循环体里用if/elif/else判断分数属于哪个区间,分别计数。整个程序不超过15行,但已经把条件语句、循环语句、变量累加这些基础要素全用上了。类似这样的需求,在真实的数据清洗、报表统计、日志分析中比比皆是。学会条件和循环,就是学会用代码解决这类“重复+判断”问题的能力。
2. if / elif / else:Python里最常用的逻辑分支工具
条件语句在Python里的主力就是if、elif、else这三个关键字。你可能早就见过它们,但这里我想把每个细节和常见误区都摊开讲清楚,因为这些细节决定了你的代码是否健壮。
2.1 if语句的基础形态
最简单的if语句长这样:
score = 85 if score >= 60: print("及格了")这里有一个非常重要的细节:Python用缩进来表示代码块,而不是像C语言或Java那样用花括号。if后面跟一个条件表达式,条件为真(True)时,执行下面缩进的代码;条件为假(False)时,跳过缩进块继续往下走。
缩进通常用4个空格,同一个代码块内必须保持一致。混用Tab和空格会直接报IndentationError,这是新手最容易踩的坑之一。我的建议是:编辑器里把Tab键自动替换为空格,一劳永逸。
条件表达式可以是任何“能计算出真假值”的东西,不一定是比较运算。比如:
name = "Alice" if name: # 字符串非空即为True print("名字不是空的")Python中,数字0、空字符串""、空列表[]、空字典{}、None等都被视为“假值”,其他值都视为“真值”。这个“真值测试”机制非常实用,能让代码更简洁。
2.2 elif是“否则如果”,不是“再判断一次”
很多人写多个分支时,喜欢写成一连串独立的if:
score = 75 if score >= 90: print("优秀") if score >= 60: print("及格") if score < 60: print("不及格")这段代码能跑,但存在严重问题:score = 75时,score >= 90为假,但score >= 60为真,会打印“及格”;这看起来没问题。可一旦score是95,第一个if打印“优秀”,第二个if又因为95 >= 60为真再打印“及格”——一个分数被判了两次。这就是独立if和elif的本质差异:elif只在前面所有条件都不成立时才会继续判断,保证多个分支里只执行一个。
score = 95 if score >= 90: print("优秀") elif score >= 60: print("及格") else: print("不及格")运行结果只输出“优秀”。else是兜底分支,前面所有条件都不满足时执行,它不需要写条件表达式。
判断顺序也有讲究。条件要按“从窄到宽”或“从特殊到一般”排列。还是拿成绩举例,如果把score >= 60写在最前面,那score = 95会先命中“及格”,后面的“优秀”分支永远执行不到。这个坑非常隐蔽,逻辑上是“写完能跑”,但结果完全不对。我见过不止一个初学者在这里栽过跟头,排查半天才发现是分支顺序错了。
2.3 比较运算符的完整清单
条件表达式离不开比较运算符,Python中常用的有以下这些:
| 运算符 | 含义 | 示例 | 结果 |
|---|---|---|---|
== | 相等 | 3 == 3 | True |
!= | 不相等 | 3 != 4 | True |
> | 大于 | 5 > 3 | True |
< | 小于 | 5 < 3 | False |
>= | 大于等于 | 5 >= 5 | True |
<= | 小于等于 | 5 <= 4 | False |
is | 身份比较 | a is None | 见下文说明 |
in | 成员判断 | "py" in "python" | True |
is和==的区别需要特别留意。==比较的是“值是否相等”,is比较的是“两个变量是否指向同一个对象”。对于整数和短字符串这类小对象,Python有缓存机制,a = 1; b = 1; a is b通常为True,容易让人误以为is和==等价。但遇到大整数、长字符串或自定义对象时就不一样了。安全规则是:判断值用==,判断是否为None用is None。
2.4 逻辑运算符 and / or / not 的短路行为
多个条件需要同时满足或满足其一,就用逻辑运算符组合:
age = 25 salary = 8000 if age < 30 and salary > 5000: print("符合初筛条件")and表示两边都为真才为真,or表示至少一边为真即为真,not表示取反。这三个运算符还有一个很重要的特性:短路求值。程序执行a and b时,如果a已经是False,那不管b是什么,整个表达式都为False,Python就直接返回a,不再计算b;同理,a or b中如果a是True,也直接短路。
短路特性在日常编码中特别有用,可以用来避免一些错误。比如你想判断一个列表非空后再取第一个元素:
items = [] if items and items[0] == "apple": print("第一个水果是苹果")如果items是空列表,items本身为假,短路后根本不会执行items[0],也就不会触发“索引越界”异常。如果写成独立的两个if,不先判空就访问下标,程序必然报错。这就是“为什么别人写的代码那么稳”的底层原因之一——懂得用短路控制执行时机。
2.5 三元表达式与match-case模式匹配:让代码更简洁的高级写法
除了普通的if语句,Python还提供了两种简化写法。
第一种是三元表达式,也叫条件表达式,适合赋值时二选一:
status = "成年" if age >= 18 else "未成年"这行代码等价于:
if age >= 18: status = "成年" else: status = "未成年"三元表达式能让代码更紧凑,但不建议在复杂的嵌套场景里硬用——嵌套三元表达式可读性极差,写出来别人看不懂,一个月后的你自己也看不懂。
第二种是Python 3.10之后引入的match-case,也叫结构模式匹配。它的作用类似其他语言中的switch-case,但功能更强大,可以匹配数值、字符串、数据结构:
command = "start" match command: case "start": print("启动") case "stop": print("停止") case "restart": print("重启") case _: print("未知命令")case _相当于else兜底。对于“一个变量有多种取值且不同取值做不同事”的场景,match-case比连续if/elif可读性更好。但要注意:match-case的匹配是精确匹配,不是大于小于这种范围判断。如果想判断范围,还是老老实实用if。我的判断标准是:分支小于三个用if/elif,分支多且匹配固定值时用match-case。
3. while与for:两类循环,两种思维
条件解决“往哪走”的问题,循环解决“走几遍”的问题。Python里有两类循环:while和for。很多人觉得自己会写for就会循环了,其实这两类循环的适用场景完全不同,理解差别才能写出高效的代码。
3.1 while循环:不知道多少次时用
while语法如下:
count = 0 while count < 5: print("当前计数:", count) count += 1执行逻辑是:先判断条件count < 5,为真就执行循环体,执行完再次回到条件判断,直到条件为假退出。while循环的本质是“只要条件成立,就一直执行”,适合循环次数事先未知的场景。
典型例子是用户输入验证。你希望用户输入一个合法的数字,在用户输对之前一直循环:
num = -1 while num < 0 or num > 100: num = int(input("请输入0到100之间的数字: ")) print("你输入的是:", num)这里没法提前确定循环会执行几次——可能一次就成功,也可能要问好几遍。用for反而不方便。
需要特别注意的一点是:while循环里一定要有一条语句能改变条件结果,否则就是死循环。上面的count += 1就是在推进循环朝退出方向发展。现实中,忘记写count += 1导致程序卡死的情况非常多,进程只能强制结束。如果你在脚本里写了while,跑起来发现控制台疯狂输出不停,九成是循环变量没更新。
3.2 for循环:遍历一切可迭代对象
for循环在Python里远比在C语言里强大。C语言的for其实是“带条件的循环”,而Python的for是“遍历可迭代对象”:
fruits = ["苹果", "香蕉", "橙子"] for fruit in fruits: print(fruit)这个例子中,每次循环从列表里取出一个元素,赋值给变量fruit,然后执行循环体。不仅仅是列表,字符串、元组、字典、集合、文件对象、range对象,凡是“可以被迭代”的东西,都可以用for遍历。
实际使用中,我推荐一个原则:能遍历就遍历,不要自己数下标。很多从C或Java转来的程序员习惯写for i in range(len(list)): print(list[i]),这在Python里并不优雅。直接for item in list更简洁而且不容易出错。当确实需要下标时,用enumerate一次拿到索引和值。
3.3 range、enumerate、zip:三个高频帮手
range是生成整数序列的核心函数。range(start, stop, step)生成从start开始、到stop之前结束、每次增加step的序列。注意stop是开区间,不包含:
for i in range(1, 10, 2): print(i) # 输出 1 3 5 7 9enumerate用于在遍历时同时拿到索引:
fruits = ["苹果", "香蕉", "橙子"] for idx, fruit in enumerate(fruits): print(idx, fruit) # 输出: # 0 苹果 # 1 香蕉 # 2 橙子zip用于并行遍历多个序列:
names = ["小明", "小红", "小刚"] scores = [92, 85, 78] for name, score in zip(names, scores): print(name, score) # 输出: # 小明 92 # 小红 85 # 小刚 78这三个函数用好了,循环代码的质量会明显提升。它们解决的是“不需要手动管理索引”的遍历需求,代码更短、边界错误更少。
3.4 break / continue / else:循环的三板斧
循环体里可以使用两个控制关键字:
break:立即终止整个循环,跳到循环后面的代码。continue:跳过本次循环的剩余部分,直接进入下一次迭代。
一个经典的break用法是在数据中查找目标:
numbers = [3, 7, 12, 9, 15] for num in numbers: if num == 12: print("找到了目标:", num) break循环可以提前结束,也可以自然跑完。Python的循环还有一个很多语言没有的特性:else子句。它的语义是:循环正常结束(没有被break打断)时,执行else块。
for num in range(3, 20): if num % 7 == 0: print("找到7的倍数:", num) break else: print("没有找到7的倍数")这个特性很实用,可以用来判断“是否找到了目标”。如果找到了,break触发,else不执行;如果找遍了都没有,else执行。逻辑上其实等价于设置一个found标志位,但else写法更简洁、语义更明确。不过要注意,很多人第一次看这个语法会觉得反直觉,建议自己在代码里多写两遍建立手感。
4. 循环的效率与架构设计:嵌套循环、提前退出和替代方案
循环谁都会写,但“写得高效”“写得稳”就是另一回事了。这一节我聊几个实战中频繁遇到的循环结构问题:嵌套循环怎么组织、什么时候提前退出、哪些循环其实可以用更优雅的方式替代。
4.1 嵌套循环:多层遍历时的执行顺序
循环里套循环,就是嵌套循环。最常见的场景是处理二维数据——比如一个矩阵、一份表格里的多行多列:
matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] for row in matrix: for col in row: print(col, end=" ") print() # 每行结束换行执行顺序遵循“外层循环走一次,内层循环走完整一遍”。上面代码中,外层每取一行,内层就遍历这一行的所有元素。这个执行逻辑一定要想清楚,因为嵌套循环的时间复杂度是相乘关系——外层循环n次、内层循环m次,总计执行n乘m次。
嵌套层数越多代码越难读,也越容易出问题。我的经验是:超过两层嵌套,就要考虑拆函数或换数据结构。三层以上的嵌套循环,无论是调试还是后续维护,都会让你怀疑人生。很多时候,把内层逻辑提取成一个小函数,代码会清爽很多。
4.2 提前退出:用break和条件判断减少无效计算
循环在满足某个条件时是可以提前撤的,不是每次都必须跑完。比如你想找一组数字中第一个大于100的数:
numbers = [45, 89, 23, 156, 78, 200] for num in numbers: if num > 100: print("找到第一个大于100的数:", num) break如果没有break,循环会遍历完整个列表,多做了好几轮无效计算。在数据量小的时候无伤大雅,但处理百万级、千万级数据时,一次提前退出可能节省几秒甚至几十秒的时间。能不做的计算就不要做,能提前退出的循环就不要跑满,这是性能优化最基本的意识。
4.3 会写循环也要会“跳出循环”:列表推导式与生成器
有些循环,表面上看是循环,但实际上是在“生成一个新列表”。Python提供了列表推导式这种更高效、更简洁的写法。
拿“筛选并转换数据”举例。传统写法:
scores = [55, 82, 90, 47, 73] upgraded = [] for score in scores: if score >= 60: upgraded.append(score + 5) print(upgraded)用列表推导式:
scores = [55, 82, 90, 47, 73] upgraded = [score + 5 for score in scores if score >= 60] print(upgraded)两段代码结果完全一样,但第二段只用了一行,可读性和性能都更好。列表推导式的语法就是“表达式 + for子句 + if条件”,它让代码平铺直叙地描述“我要什么”,而不是啰嗦地解释“怎么循环怎么添加”。
如果你处理的数据量很大,更推荐生成器表达式:
upgraded = (score + 5 for score in scores if score >= 60)生成器是惰性求值的——它不会一次性把所有结果算出来存在内存里,而是每次迭代时算一个值。在处理几百万元素的列表时,这个差异在内存占用上非常明显。我的建议是:租出列表推导式替换简单的“循环+append”场景,数据量不确定时优先考虑生成器。
5. 条件与循环的实战组合:三个经典问题从暴力到优雅
学语法最忌讳“看了就会,一写就废”。这一节我把条件与循环组合起来,通过三个经典问题演示完整的解题思路。每个问题我都会从需求分析讲到代码实现,再讲优化方向,让你看看真实开发中是怎么一步步把代码打磨出来的。
5.1 经典案例:判断质数的完整思路
质数的定义是“大于1的自然数中,只能被1和自身整除的数”。判断一个数n是否为质数,最朴素的方法是:从2到n-1逐个检查,看是否存在能整除n的数。
n = 29 is_prime = True for i in range(2, n): if n % i == 0: is_prime = False break if is_prime: print(n, "是质数") else: print(n, "不是质数")代码本身不难,但这里有几个可优化的点:
第一,只需要检查到根号n就够了。因为如果n有一个大于根号n的因数,那必然同时存在一个小于根号n的因数,提前检查到根号n已经覆盖了所有配对情况。
import math n = 29 is_prime = True for i in range(2, int(math.isqrt(n)) + 1): if n % i == 0: is_prime = False break别看只改了一点,当n是十位数级别的大数时,检查范围从n缩小到根号n,计算量呈指数级下降。在算法里,减少循环范围是仅次于提前退出的第二大优化手段。
第二个细节是对2和小于2的数的处理。2是质数,1不是质数,负数也不是质数。所以严谨的代码开头需要有边界判断:
if n <= 1: is_prime = False elif n == 2: is_prime = True这种边界条件的处理和分支顺序,正是体现代码质量的地方。很多“看起来能跑但有隐藏bug”的代码,都是在边界值上翻的车。
5.2 表格输出与九九乘法表
条件与循环结合最多的一类场景就是格式化输出。九九乘法表是训练嵌套循环逻辑的经典题目:
for i in range(1, 10): for j in range(1, i + 1): print(f"{j} x {i} = {i * j}", end="\t") print()注意内层循环的范围是range(1, i + 1)——行数越大,列数越多。这里的关键是内层循环的范围依赖外层循环的变量,这是嵌套循环最常见的写法。
运行结果第一行只有一个1 x 1 = 1,第二行有1 x 2 = 2和2 x 2 = 4,依此类推。每种编程语言都在练这道题,因为它同时考察了循环嵌套、变量依赖和格式化输出三个能力。
实际开发中,类似的“表格输出”需求非常常见。比如输出库存数据、成绩表、账单明细,逻辑都是外层遍历行、内层遍历列。掌握这种模式,你就在很多日常报表任务中直接套用。
5.3 猜数字游戏:把条件与循环串起来的完整程序
猜数字游戏是一个“小而全”的综合练习:程序随机生成一个1到100的整数,用户输入数字,程序提示“大了”“小了”,直到猜中为止。
import random target = random.randint(1, 100) guess = 0 count = 0 while guess != target: guess = int(input("请输入你猜的数字(1-100): ")) count += 1 if guess > target: print("猜大了,再试试") elif guess < target: print("猜小了,再试试") else: print(f"恭喜你!猜了{count}次就中了")这段代码用一个while循环统筹整个游戏过程,循环条件就是“没猜中”,循环体里用if/elif/else区分三种情况。条件与循环的配合在这个例子里体现得淋漓至尽——循环负责“反复玩”,条件负责“每次玩的结果反馈”。
这里有一个容易被忽视的点:用户输入的是字符串,int()转换如果遇到非数字内容会直接抛异常。想让程序更健壮,可以从加try/except异常处理。异常是另一个完整的主题,但不影响你现在理解这个程序的核心结构。
5.4 数据清洗中的条件筛选场景
回到一开始提到的成绩统计,把完整代码写一遍,你会发现条件与循环组合后能解决的实际问题比想象中多:
scores = [55, 82, 90, 47, 73, 68, 99, 61, 38, 75] excellent = 0 passed = 0 failed = 0 for score in scores: if score >= 90: excellent += 1 elif score >= 60: passed += 1 else: failed += 1 print(f"优秀: {excellent}人, 及格: {passed}人, 不及格: {failed}人")这段代码就是活生生的数据清洗逻辑——从一批原始数据中识别出不同类别。同样的模式可以套用到日志分析(按错误级别统计)、用户画像(按活跃度分组)、订单处理(按金额区间分类)等无数场景。条件和循环的组合,是所有数据处理逻辑的根,很多看似高大上的pandas操作,底层也是类似这样在每一个元素上做判断和归类。
6. 条件与循环的高频踩坑指南:这些坑我替你先踩过了
这一节我把自己这些年写Python遇到的、带新人时见到的各种坑集中整理一下。每个坑我当时都花了不少时间排查,写出来就是希望大家能直接绕开。
6.1 用is比较数字/字符串:一个隐蔽的等价陷阱
前面提过is和==的区别,这里展开讲一个真实案例。有一次我在排查一段数据处理代码,发现某条记录明明数值相等却一直被判断为不匹配。最后定位才发现,代码里写的是if result is 100,而不是if result == 100。
原因在于Python对小整数有缓存机制——通常-5到256之间的整数会被复用同一个对象,所以a = 100; b = 100; a is b这个测试偶尔是True。可一旦数值超出缓存范围,或者计算方式不同(比如100.0这种浮点数),is比较就会返回False。在条件判断里,千万不要用is去比较数值和字符串。is只用于判断是否为None,或者是否为某个单例对象,其余一律用==。
6.2 在循环中修改正在遍历的列表
有一次我需要从列表里删掉所有小于60分的成绩,顺手写了:
scores = [55, 82, 90, 47, 73, 68] for score in scores: if score < 60: scores.remove(score)结果发现输出完全不对。原因是在遍历列表时删除元素会改变列表的索引结构——删掉一个元素后,后面的元素整体前移,导致某些元素被跳过,没有参与判断。这种bug表现得很诡异,而且数据量越大越难发现。
推荐的稳妥做法是遍历原列表的副本,或者用列表推导式生成新列表:
scores = [55, 82, 90, 47, 73, 68] scores = [score for score in scores if score >= 60]列表推导式会生成一个筛掉不合格元素后的新列表,完全避开“遍历时修改原列表”的问题,代码也更简洁。这个坑在数据处理中非常常见,我强烈建议养成“能不原地改就不原地改”的习惯。
6.3 死循环与循环变量泄漏:看似小问题的严重隐患
死循环是最常见的循环事故。典型原因是while循环没有更新循环变量:
i = 0 while i < 10: print(i) # 忘记写 i += 1这段代码会无限打印0,直到你杀掉进程。调试这类问题的过程中,程序会一直输出导致日志暴涨甚至磁盘占满。我现在写while循环时,习惯先写好循环变量的更新语句再写业务逻辑,把“推进循环”当成第一要务。
另一个问题是循环变量的泄漏。Python的for循环在循环结束后,循环变量依然存在且保持最后一次的值:
for i in range(5): pass print(i) # 输出4这个特性在某些场景会引发隐蔽的bug——如果你后面意外用到了i,拿到的可能是循环遗留值而不是你预期的值。尤其在Jupyter Notebook这类交互环境中,多余变量很烦人。虽然没有强制规定,但我的习惯是在循环之后如果不再需要i,就直接del i,避免污染后续代码。
6.4 浮点数比较:你以为相等,其实不相等
如果你用浮点数做条件判断,会遇到一个很经典的坑:
if 0.1 + 0.2 == 0.3: print("相等") else: print("不相等") # 实际输出这个因为浮点数在计算机中是用二进制近似表示的,0.1 + 0.2的结果实际上是0.30000000000000004,并不精确等于0.3。用==直接比较浮点数是非常危险的做法。
解决方案是用“差值范围”比较:
if abs((0.1 + 0.2) - 0.3) < 1e-9: print("近似相等")或者,在金额计算这类要求精确的场景,直接用decimal.Decimal代替浮点数。这个坑尤其影响的是涉及统计计算、数据处理、财务分析的程序。记住一句话:浮点数用区间判断,不要用等号。
6.5 条件分支里的“少用else”原则
最后分享一个代码风格上的心得。很多初学者写if时习惯性加个else,但有些情况else会掩盖bug。比如:
score = 85 if score >= 60: print("及格") else: print("不及格")这段代码看起来没问题,但如果你在中间加了一个新的条件分支,比如“优秀”的判断,很可能会把else的范围理解错,导致逻辑混乱。我现在的习惯是:能用elif明确区分就尽量写elif,尽量让每个分支都有明确的条件,减少对“兜底”的依赖。把逻辑写“显式”而不是“隐式”,是提高代码可维护性最重要的一步。
踩过这些坑之后,我对条件和循环的理解更靠近“防御式编程”——不要假设输入总是正常的、不要假设数据总是完整的、不要假设边界值不会出现。把这些假设在条件判断里显式写出来,你的代码会稳很多。