ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python条件与循环详解:从基础语法到工程避坑指南

Python条件与循环详解:从基础语法到工程避坑指南 很多朋友初学 Python 的时候都有一种错觉语法这么简单看两眼就会了。可一旦真去写一个自动化脚本、做一个数据处理或者写个爬虫就发现代码开始不听话。其实十个人里有八个问题不在语法本身而在于看到if、for、while的时候脑子里没有一张清晰的“执行逻辑图”。这一篇我准备把 Python 的条件语句和循环语句一次性讲透。会从最基本的写法讲到工程里常见的坑再给一些可以照抄的实操案例。不管你是刚入门想补基础还是写过一阵子想系统梳理控制流这篇都值得收藏跟着敲一遍。毕竟在 Python 入门、Python 基础语法这些话题里真正能卡住新手的地方百分之八十都集中在条件判断和循环控制上。1. 条件与循环的底层逻辑为什么语法简单却威力巨大1.1 程序流程的三种结构条件循环属于哪一环任何程序不管多复杂本质上都由三种结构组合而成顺序结构、选择结构、循环结构。顺序结构就是一行一行从上往下执行这个不用多解释选择结构就是“看情况走哪条路”对应到 Python 就是if条件语句循环结构就是“反复做同一件事”对应到 Python 就是for和while循环语句。理解了这一点你再看 Python 代码就不会觉得章节是散的。一个 web 接口要校验参数那层校验逻辑本质就是条件语句一个量化策略要回放历史行情那层 K 线遍历本质就是循环语句爬虫去翻页抓数据看起来又是循环又是条件其实也只是把这两种结构嵌套在一起用。所以我会建议所有学 Python 的人不要为了写循环而写循环也不要把条件语句当成语法知识点去背。你应该想清楚一件事代码在哪个节点需要做决策在哪个范围内需要重复执行一旦这两件事想明白了代码结构自己就浮出来了。1.2 这篇文章适合谁看完能解决什么问题如果你刚开始接触 Python刚看完变量、数据类型那几章那这篇正好可以帮你把流程控制的地基打牢。条件语句和循环语句是继变量之后最核心的“语法骨架”所有后续的函数、类、文件操作、异常处理都建立在它们上面。如果你已经写过一段时间 Python但总觉得代码重复度高、逻辑绕、改起来头疼这篇里的常见坑和工程习惯也会很有帮助。比如遍历列表时修改列表导致的诡异结果、while循环忘记写退出条件导致程序卡死、条件判断的顺序不对导致结果和预期反着来——这些问题在各类 Python 教程里经常被一笔带过但实际项目里非常常见。我写的实操案例会贴近真实场景比如数据清洗里的空值过滤、批量计算里的遍历、交互输入里的重试处理。如果你后续想做数据分析、爬虫、自动化脚本这些模式基本都会遇到。2. 条件语句全解析if、elif、else 的写法与思考方式2.1 基础语法缩进、执行时机、真值判断先看最简单的结构score 78 if score 60: print(及格了) print(继续保持) print(不管怎样都会执行)这段代码有两个关键点要注意。第一Python 用缩进表示代码块。if后面的print(及格了)和print(继续保持)前面有缩进它们属于if满足条件时才执行的块最后一个print没有缩进所以不管条件是否成立都会执行。这是 Python 和 C、Java 用大括号{}表示代码块的一个很大区别。不少从其他语言转过来的朋友写 Python 时最喜欢犯的错就是缩进长度不统一明明看起来对齐了一运行爆个IndentationError。第二条件语句在运行到这里时只会检查一次。if score 60这个判断成立之后直接进入分支块执行执行完跳出整个 if 结构继续往下走。它不会像循环那样多次检查。所以你要是想写出“每隔一段时间判断一次”的逻辑不能光靠if得搭配循环或者定时器。再聊聊 Python 的真值判断。条件不一定非要比较运算符可以直接写一个变量。Python 里None、0、0.0、空字符串、空列表[]、空字典{}、空集合set()都会被当成False叫“假值”其他情况都算True。所以很多老 Python 程序员写文件读取判断时会直接写data get_data() if data: print(有数据开始处理) else: print(没有数据)这不只是简化写法而是一种约定俗成的习惯。你在看别人代码时要是看到if not data、if items这种要立刻反应过来这是在判断“是否为空”而不是在做数值运算。2.2 三元表达式、链式比较与布尔运算优先级的坑条件语句除了完整的if ... elif ... else写法还有几个高频出现的变体。三元表达式长这样status 成年 if age 18 else 未成年这个表达式的意思是如果age 18成立就把成年赋给status否则赋未成年。它是为了处理“二选一赋值”这种简单场景。可读性比写一整段 if/else 更好但千万别在里面套复杂逻辑否则别人读代码的时候只想打人。链式比较是 Python 一个讨喜的小设计比数学还要直观if 18 age 60: print(青年至中年阶段)这段等价于if age 18 and age 60。Python 允许这种写法其实底层是把比较拆成两次但逻辑上更符合日常表达。说到and和or这里有个非常容易踩的优先级坑。and的优先级高于or。看这个例子a True b False c False result a or b and c因为先算b and c得到False再算a or False得到True。如果你本来想表达(a or b) and c那结果就完全不一样了。我见过不少人在写复杂权限判断时栽在这里。解决方案很土但有效拿不准的时候加括号。代码里多几层括号不丢人逻辑错了才丢人。2.3 实操案例用条件语句做业务评分和数据清洗举一个典型的业务评分案例。假设要根据考试成绩划分等级90 以上为 A80 到 89 为 B70 到 79 为 C60 到 69 为 D其余为 Edef get_grade(score): if score 90: return A elif score 80: return B elif score 70: return C elif score 60: return D else: return E print(get_grade(82)) # B print(get_grade(59)) # E写这种连续分级的判断时顺序很重要。要按范围从大到小写或者从小到大写千万不要把顺序写乱。比如你先把score 80写在前面那 90 分也会进入这个分支输出 B结果就错了。这正是条件语句里最典型的“执行顺序”问题elif是按顺序依次检查的一旦某个条件成立后面的elif通通不会再看了。再举一个数据清洗场景。假设你拿到了一个含混合类型的列表里面可能有空字符串、None、数字和布尔值现在要筛出可以参与数值计算的元素raw [3, None, , 0, 5.5, False, 8, ] clean [] for item in raw: if isinstance(item, str) and item.strip(): clean.append(float(item)) elif isinstance(item, (int, float)) and not isinstance(item, bool): clean.append(item) print(clean) # [3.0, 5.5, 8.0]这段里有两个值得注意的细节。第一空字符串和只含空格的字符串要用.strip()处理因为是假值没错但 是真值直接if item会漏掉这类脏数据。第二Python 里bool是int的子类isinstance(True, int)成立所以要把布尔值单独排除掉。这种细节不对照实际数据搞一次光看教程真发现不了。3. 循环语句之 for从遍历到批量处理3.1 for 循环的运行机制与 range() 函数的用法for循环是 Python 里最常用的循环语句但它的底层思维和 C 语言不太一样。C 语言里的for更像“执行 N 次”而 Python 的for本质是“遍历一个可迭代对象”。字符串、列表、元组、字典、集合、文件对象只要能被迭代都能放进for里。for name in [小明, 小红, 小刚]: print(f你好{name})这个循环每轮从列表里取一个元素赋给变量name然后执行缩进块里的代码。取完最后一个元素循环自动结束。很多新手刚接触循环时喜欢用下标遍历fruits [苹果, 香蕉, 草莓] for i in range(len(fruits)): print(i, fruits[i])这个写法没错但不够 Pythonic。range(len(...))是 C 时代思维的遗留在 Python 里一般会用enumerate替代后面我会专门讲。不过range()本身是个高频工具还是得掌握它的三种用法。range(stop)表示从 0 到 stop-1比如range(5)得到 0、1、2、3、4range(start, stop)表示从 start 到 stop-1range(start, stop, step)里的 step 可以是正数也可以是负数。比如range(1, 10, 2)得到 1、3、5、7、9range(10, 0, -2)得到 10、8、6、4、2。需要注意的是range(5)生成了一个range对象它不是列表但支持迭代。判断一个数在不在某个等差序列里也可以用in操作因为range实现了相应的成员判断逻辑效率也很高。3.2 遍历增强技巧enumerate、zip、dict.items()光会for i in xxx只能算入门真正写项目时会经常需要同时拿元素和索引、同时遍历两个列表、或者遍历字典的键和值。enumerate解决“既要索引又要元素”的问题products [手机, 电脑, 耳机] for idx, product in enumerate(products, start1): print(idx, product)第二个参数start1很实用写报表、打印序号时不用自己手动维护计数器了。你可能会问Python 里有没有类似i的写法没有因为迭代才是 Python 的风格。每次循环从可迭代对象里取出一个值不需要你操心自增的问题。zip解决“同时遍历多个列表”的问题names [张三, 李四, 王五] scores [88, 92, 73] for name, score in zip(names, scores): print(f{name}: {score})这里zip会把两个列表按位置“拉链”一样拼起来每轮循环拿到一个名字和对应的分数。它的余数是 Python 一个很经典的行为当两个列表长度不一致时zip以短的为准。如果你希望以长的为准缺失的位置用默认值补齐那要用itertools.zip_longest不过我实际项目中用到它的频率不高。遍历字典是另一个高频场景。直接for key in my_dict拿到的是键要拿值就用.values()键和值都要用.items()user_info {name: 王五, age: 25, city: 成都} for key, value in user_info.items(): print(key, , value)这里需要注意Python 3.7 之后字典能保证插入顺序所以遍历顺序和插入顺序一致。这在以前可是没有任何保证的如果你在维护老项目遇到字典遍历乱序的问题升级版本后多半会好。3.3 实操案例用 for 循环批量处理结构化数据for循环最大的价值在于批量操作。我拿一个常见的“结构化数据”处理场景来演示。假设你从接口拿到一批用户数据每个用户是一个字典字段有name、age、spend现在要做两件事筛掉未成年用户对剩余用户的消费额做汇总。users [ {name: 张三, age: 25, spend: 120.5}, {name: 李四, age: 17, spend: 89.0}, {name: 王五, age: 30, spend: 230.0}, {name: 赵六, age: 16, spend: 45.5}, ] total 0 valid_count 0 for user in users: if user[age] 18: continue total user[spend] valid_count 1 print(f成年用户数: {valid_count}总消费额: {total:.2f})这里用到了continue它表示“跳过本轮循环剩余代码直接进入下一轮”。continue在处理脏数据时非常好用能让循环体减少一层嵌套。如果你想表达“未成年用户不参与统计”的逻辑写continue永远比写一大段if user[age] 18:再包住后面所有逻辑更清晰。我操盘真实数据时还习惯在循环里加一个计数器或者打点日志否则处理了几万条数据后哪里算错了根本无从查起。上面的valid_count不只为最后输出服务更是调试时确认循环分支是否走了预期路径的依据。4. 循环语句之 while条件驱动型循环的控制与退出4.1 while 循环的设计思路什么时候用 while什么时候用 forfor循环适合遍历已知集合或已知次数而while循环适合“条件为真就继续跑”的场景它不关心集合只关心判断条件。最简单的形态count 0 while count 5: print(当前计数:, count) count 1这个循环的执行逻辑是先检查count 5如果成立就执行块内代码执行完回到条件处再检查。count从 0 一直加到 5条件变为假循环停止。实际操作里while更适合用在“次数事前不确定”的场景比如等待某个服务就绪、不断重试网络请求、循环读用户输入直到合法为止。这个划分听起来简单但新手很容易把两者用错。我见过有同学把for i in range(100):写成while i 100:却忘了在循环体里更新i结果程序直接卡死。这就是典型的“拿 while 干 for 的活”造成的悲剧。4.2 break、continue、else 的高频组合除了continue另一个控制循环的关键词是break。break表示“立即结束整个循环”后面的代码不再执行循环也不再继续。一个经典场景是搜索列表里的第一个目标值numbers [3, 7, 9, 12, 15] for num in numbers: if num % 3 0 and num % 5 0: print(找到了:, num) break这里找到 15 之后就退出循环不会继续遍历后面的元素。如果列表很长break能有效减少无效迭代。很多新手不知道的一点是break只跳出它所在的最内层循环不负责跳出外层。要跳出多层嵌套就得用标志位或者把逻辑封装成函数用return结束。while和break的组合则是“重试机制”的标配。比如写一个获取用户输入的小程序要求必须输入非空内容否则一直问while True: text input(请输入内容输入 q 退出: ).strip() if text q: print(用户主动退出) break if text: print(收到内容:, text) break print(输入不能为空请重试)while True是一个刻意构造的无限循环它的退出完全依赖break。这种写法在处理交互输入、网络重试时很常见因为它让逻辑集中在循环体内判断而不是在循环条件处堆一堆判断式。再说说 Python 里for和while都能配的else子句。这个特性让很多人迷惑因为else太容易联想到if。它的规则是循环正常结束没有被break提前打断时执行else块。for num in [2, 4, 6]: if num % 3 0: print(存在能被3整除的数:, num) break else: print(列表中没有能被3整除的数)这段代码因为6能被 3 整除break触发else不执行。如果列表是[2, 4, 8]循环跑完没中断else才打印提示。这种写法在“检查是否存在满足条件的元素”的场景里很顺手比专门设一个found标志位再判断要干净。4.3 防止死循环工程上的三道防线while最大的风险就是死循环。程序跑起来不停机CPU 直接拉满排查起来也麻烦。写生产环境的代码我会习惯性给while加三道防线。第一道防线初始化条件变量并确保条件变量在循环体内能被修改。前面说的count 1就是最基本的一种很多人漏掉这一步就跑了。第二道防线设置最大次数上限。不管什么场景我都喜欢给循环加一个保护MAX_RETRY 5 retry_count 0 success False while not success and retry_count MAX_RETRY: retry_count 1 print(f第 {retry_count} 次尝试) success do_something() if not success: print(多次尝试后仍失败记录日志)这个模式在调用外部接口、写爬虫、连接数据库时特别实用。与其让程序无限等下去不如明确告诉它“试 5 次不行就算了”。任何依赖外部资源的循环都应该有类似的上限机制。第三道防线在循环里打印或记录日志。死循环最怕的不是死而是你不知道它死在哪个环节。适当输出当前状态比如处理到第几条数据、当前计数是多少能在程序不规范响应时快速定位。在实际开发中我见过因为忘记更新条件变量导致服务器端进程卡死的案例。有时候不是大家不知道更新而是代码逻辑复杂多个分支里有的分支漏了更新。出现这种情况最好反思一下这个循环的条件是不是太复杂了能不能把循环更新逻辑统一放到循环体最后或者干脆换一种遍历方式5. 流程控制的进阶组合与效率优化5.1 让代码更简洁列表推导式与生成器替代嵌套循环写循环写多了你会发现有些循环就是“从一组数据里变换出另一组数据”这种场景在 Python 里可以用列表推导式一步完成。比如要把一个列表里的每个数字翻倍nums [1, 2, 3, 4, 5] doubled [n * 2 for n in nums] print(doubled) # [2, 4, 6, 8, 10]这个写法本质上等价于doubled [] for n in nums: doubled.append(n * 2)列表推导式看起来精简很多底层执行效率通常也更高因为 Python 内部对它做了专门优化。它还可以加条件过滤nums [1, 2, 3, 4, 5, 6, 7, 8] even [n for n in nums if n % 2 0] print(even) # [2, 4, 6, 8]甚至支持双层pairs [(x, y) for x in range(3) for y in range(2)] print(pairs) # [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]但这里有一个忠告列表推导式适合逻辑清晰的场景一旦推导式里嵌套超过两层或者条件表达式长得吓人宁可改回普通的for循环。代码是给人看的可读性永远要排在炫技前面。我自己写的时候会守住一个原则一个推导式超过一行放不下就考虑拆开。for循环还有一处可优化的地方就是数据量巨大时用生成器而不是列表。生成器不会一次性把所有结果放进内存。把列表推导式的中括号换成圆括号就得到生成器表达式total sum(n * n for n in range(10000000))这里range(10000000)配合平方生成如果硬生生构成长列表内存占用会非常惊人。生成器表达式让每一步计算汇总时一次只产生一个值省内存、跑得快在数据量上来的时候优势明显。5.2 循环里调用函数时要注意什么很多人在循环里调用函数时容易犯一个小毛病把费时费力的“配置加载”“连接创建”等操作不小心放进了循环体导致每轮迭代都重复执行一次。如果函数的执行结果不依赖当前迭代变量就应该把它挪到循环外面。比如下面的写法就浪费资源for user in users: db get_db_connection() # BAD! 每轮都重新建连接 save_user(db, user)正确写法应该是把get_db_connection()提到循环外面复用同一个连接。循环里调用函数还有一个隐性问题Python 的变量作用域是“函数级”不是“块级”。循环里定义的变量循环结束后还能访问。像for i in range(5): pass结束之后i的值是 4。这个特性有些新手第一次遇到会很困惑但我习惯利用这一点在循环结束后读取最后一个值做确认反而成了调试技巧。另外在循环里写lambda要注意延迟绑定的问题。举个例子funcs [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # 输出 [2, 2, 2]而不是 [0, 1, 2]原因是循环结束时i已经是 2lambda 函数体内的i是同一个变量调用时才去读取。我记得的应对办法是给 lambda 设置默认参数捕获当前值lambda ii: i。不过老实说工程中我会尽量避免在循环里构造复杂闭包真需要这样写也一定会在代码注释里说明意图。5.3 经典组合场景用状态标志让逻辑更清晰有一些业务流程无法靠单一循环或简单if表达需要引入状态标志。经典例子是“轮询直到条件成立或超时”import time deadline time.time() 10 ready False while time.time() deadline and not ready: status check_service_status() if status ready: ready True print(服务已就绪) break print(等待服务就绪...) time.sleep(1) if not ready: print(服务在限定时间内未就绪)这里两个核心标志一个是时间边界deadline一个是业务状态ready。我写这类代码时习惯把所有状态变量最好放在循环前面集中声明这样别人一进来就能看到这个循环在关心什么。等到循环结束后用ready判断最终结果。逻辑一旦复杂有个小技巧很管用给变量起“能读出意图的名字”。状态标志别叫flag叫found、success、ready这类语义明确的词。这也是控制流代码可维护性的关键。6. 常见错误排查与工程习惯实录6.1 IndentationError 与字符串缩进不一致条件语句和循环语句都靠缩进划定范围所以 Python 对缩进极其敏感。最常见的是混合使用 Tab 和空格。哪怕开头看起来一样解释器也会报IndentationError: unexpected indent。不同编辑器对 Tab 的显示可能相同但实际字符不同非常坑人。我的建议很明确项目里统一用 4 个空格做缩进。主流 Python 编辑器比如 VS Code、PyCharm都能配置“Tab 转换成空格”。把这项配置改好基本上能杜绝这个问题。如果你的代码从网上复制下来先全选格式化一遍或者直接运行black之类的格式化工具整理。6.2 遍历列表时增删元素结果总是很奇特的坑这是一个让无数新手崩溃的问题。直接讲案例nums [1, 2, 3, 4, 5, 6] for num in nums: if num % 2 0: nums.remove(num) print(nums) # 期望得到 [1, 3, 5]实际运行结果可能是[1, 3, 5]但也有可能是[1, 3, 5, 6]之类因为遍历时列表长度变了索引也跟着偏移。Python 的for在遍历时其实是在迭代一个隐式的索引删除元素会让后续元素前移导致部分元素被跳过。这类问题用文字解释不好理解但一旦你打印出“每轮遍历到哪个元素”就会豁然开朗。更稳妥的做法是遍历原始列表的副本在原列表上删除nums [1, 2, 3, 4, 5, 6] for num in nums[:]: # 遍历副本 if num % 2 0: nums.remove(num) print(nums) # [1, 3, 5]或者更符合 Python 风格的做法是过滤出一个新列表再重新赋值nums [num for num in nums if num % 2 ! 0]第二种写法避免了删除索引的种种混乱也体现了列表推导式在处理这类问题时天然的优势。6.3 条件优先级与除法类型导致的“离奇”结果条件语句里还有个容易踩的坑是运算符优先级和类型问题。比如判断一个数是否为偶数n 7 if n % 2 0: print(偶数) else: print(奇数)这段没问题。但如果有人习惯把判断写成if n % 2 0 and n 5一旦两个条件都涉及比较且没有括号就需要小心可读性。Python 的解析顺序里算术运算优先于比较运算比较运算优先于布尔运算。所以上面n % 2 0会先算n % 2再和0比较结果没问题。但是三种优先级混在一起的时候我强烈建议加括号。另一个离奇结果来自 Python 的除法。在 Python 3 里普通除法/得到浮点数整数除法//才得到整数。比如判断“这个数平均分给5个人能不能整数分完”新手可能写if total / 5 total // 5这在部分情况下成立但浮点精度会导致意外结果。最稳写法是这样if total % 5 0: print(可以平均分配) else: print(不能平均分配)类似的浮点精度问题还出现在判断累积值时。避免用判断浮点数改用abs(a - b) 1e-9或者干脆用 Decimal 处理金额计算。6.4 排查流程控制问题时的调试技巧调试控制流问题最笨但最有效的方法是“到处打印”。我见过很多人调试喜欢猜改一行跑一次跑完不对再猜一行效率极低。正确的做法是先确认“当前程序到底走到哪个分支”。具体可以这样做在循环开头打印轮次和关键变量在if/else的每个分支里打印一个标记字符。举个例子for idx, item in enumerate(data): print(f第{idx}条数据: {item}) if item[price] 0: print(命中空价格分支) continue total_price item[price]看到输出后读代码的人自然知道逻辑流是怎么回事。调试完再把打印删掉或者用日志模块替代。还有一个常用的招数把逻辑拆到独立小函数里用几组边界数据分别测试。比如你怀疑while的退出条件写错了可以单独调一个只有循环的小脚本传入不同初始值看输出。这比在几百行代码里上下翻找省时间多了。我自己的习惯是把“边界值”测一遍空列表、只含一个元素的列表、全是脏数据的列表、数值接近 0 的数据。条件语句最喜欢在这些边界上翻车测过一轮之后信心会大很多。最后再说两句个人体会代码里真正决定你的程序能不能正确完成业务逻辑的往往不是某个高深的算法而是这些最基础的条件与循环。我写多了之后有个很深的感受写循环之前先在心里模拟一次完整执行过程。比如某个变量在循环结束之后应该是什么值中间经历了几次break最极端的情况是什么把这些问题想清楚很多 bug 在写的时候就能避免。另外一个很推荐的做法是定期用代码格式化工具统一风格再给关键控制流的逻辑配上一行注释。半年前的代码回头看时如果没有注释哪怕是自己的也得花几分钟才能想起来当初为什么这么写。条件语句和循环语句是所有 Python 代码的地基地基稳了后面学函数式编程、学异常处理、学并发都会轻松不少。
返回列表