1. 流程控制不止是"语法",它决定了程序怎么"走"
先说个我经常对刚入门的朋友讲的话:Python流程控制,本质上解决的是"程序下一步该干什么"的问题。很多初学者把if、for、while当成需要背的语法模板,觉得记住写法就算学会了。但实际写项目时你会发现,真正难的从来不是怎么写,而是"为什么这么走"。
举个例子:你写一个自动拉取数据的小脚本,要对几十个接口返回的结果做判断。有的接口返回空值,有的返回错误码,有的数据格式不符合预期。这时候程序怎么走,完全取决于你的流程控制设计——先判断什么、后判断什么、异常怎么兜底、失败要不要重试。这套逻辑理顺了,脚本才谈得上稳定。理顺不了,代码能跑,但一遇到脏数据就崩。
这篇文章我想把Python流程控制讲透。不是罗列语法,而是从执行逻辑、常见误判、踩坑经验三个角度来拆,覆盖条件分支、循环、模式匹配、异常处理以及它们组合使用的实战场景。适合刚学Python不久、想把基础打牢的初学者,也适合写过一阵子但总觉得代码逻辑不够清爽的朋友查漏补缺。
1.1 代码不是"从上到下"读完的,是"按你的设计"跑完的
很多人对程序的执行顺序有个直觉:一行一行往下走。这句话对了一半。顺序执行确实是基础,但一旦加入条件判断和循环,程序早就不是直线了,而是变成了"带岔路的地图"。
我用一个很生活化的类比来解释。你去公司上班,正常情况下路线固定:出门、坐车、进楼、打卡。但有一天出门发现下雨了,你可能先判断"雨大不大",大就打车,不大就坐公交;到了公司再判断"电梯排队人多不多",多就爬楼梯。你看,每一步都是"条件 + 选择"。Python的流程控制就是把这套判断逻辑翻译给机器听。
所以学习流程控制时,脑子里要有一个模型:程序执行的过程,是沿着一条带有分支和回路的路径在走。if是岔路口,for/while是回路,break/continue是在回路上开的口子。把这个模型建立起来,再去写代码,思路会清晰很多。
1.2 流程判断的地基:比较运算、真值判断与短路逻辑
任何流程控制都离不开条件表达式,而条件表达式的核心是布尔值:True或False。Python里几乎任何对象都可以直接放在条件里做真值判断,但这里有非常多的隐性规则,新手特别容易踩。
看这个例子:
data = [] if data: print("有数据") else: print("空列表")这段代码会输出"空列表",因为空列表的真值是False。这个特性在Python里非常重要——你不用显式写len(data) > 0,直接if data:即可。同样,None、0、0.0、空字符串""、空字典{}、空集合set(),在布尔上下文里都是False。
还有一个重要概念是短路运算。and和or在Python里不只是返回True/False,它们会返回参与运算的某个值:
name = user_input or "默认值"这里如果user_input为空字符串(False),or就会返回右边的"默认值"。这个写法在设置默认参数、兜底赋值时非常常用。理解短路逻辑,你会少写很多if。
1.3 缩进是Python的语法边界,不是排版习惯
这一点我每次都要强调,因为它太重要了。Python用缩进表示代码块归属,不像C语言有花括号。同样的缩进层级,代表同一个代码块;缩进不一致,直接报IndentationError。更隐蔽的问题是混用Tab和空格——看起来缩进一样,但Python会报错,或者更糟糕的是在不同环境下时好时坏。
我建议所有初学者做两件事:第一,编辑器把Tab键自动转成4个空格;第二,统一用空格缩进,不要手动敲Tab。这个习惯看起来无关紧要,实际上能帮你省掉大量莫名其妙的报错。VSCode里设置"editor.insertSpaces": true,PyCharm默认就是空格缩进,都行。
还有一个容易被忽略的点:同一个代码块里,缩进层级必须一致。比如if下面有两行都属于这个分支,那这两行的缩进必须完全一样,不能一个4空格一个8空格。这种错误在复制粘贴代码时最容易出现,排查时也要优先看缩进。
2. 条件分支的实用写法:if/elif/else的正确打开方式
2.1 分支顺序很重要,条件匹配是"从上到下"的
if/elif/else是一组条件判断工具,执行逻辑是:从上往下依次判断,哪个条件先为True,就执行哪个分支,后面的分支不再判断。这个特性决定了条件顺序的排列是有讲究的。
看一个我实际见过的错误例子:
score = 85 if score >= 60: print("及格") elif score >= 80: print("优秀")猜猜输出什么?"及格"。因为score >= 60先被匹配了,后面的score >= 80根本没机会执行。这不是Python的问题,是条件顺序设计的问题。写多分支判断时,务必要把范围更窄、更具体的条件放在前面,或者反过来把范围更宽的放在最后。比如:
if score >= 90: print("优秀") elif score >= 80: print("良好") elif score >= 60: print("及格") else: print("不及格")我经常跟人讲一个原则:写elif的时候,心里要清楚前面的条件已经把哪些情况排除了。每个elif其实都隐含了"前面所有条件都不成立"这个大前提。
2.2 三元表达式和字典映射:让分支代码更"薄"
Python里的条件表达式(也叫三元表达式)可以在一行里完成简单的二选一:
status = "成年" if age >= 18 else "未成年"这种写法在赋值场景下很清爽,但注意别滥用。嵌套的三元表达式(a if cond1 else b if cond2 else c)可读性非常差,我看到这种代码通常建议拆成普通if。
另外一个很多人不知道的技巧是:用字典做"多分支查表"。当你的判断条件不是范围而是离散值的时候,用if/elif写一长串很啰嗦,字典映射更优雅:
def get_level(role): mapping = { "admin": 3, "editor": 2, "viewer": 1, "guest": 0, } return mapping.get(role, 0) # 查不到返回默认值0这比写四个elif清晰多了。注意我用的是.get(role, 0),它可以安全处理键不存在的情况,等价于else分支。
2.3 真值判断的三个高频坑:None、空值和is与==
第一个坑:if xxx == None。Python规范推荐写成if xxx is None。原因在于None是单例对象,is比较的是身份,==比较的是值。虽然大多数情况下效果一样,但==可能被某些对象的__eq__方法重写,产生意想不到的结果。用is None更安全,语义也更准确。
第二个坑:误把"有值"当成"为真"。比如你判断一个列表是否为空,if list_data:是推荐写法,这没问题。但如果你想判断"列表不为空且第一个元素符合条件",千万别写成if list_data and list_data[0] == "ok"就完了。这里的and短路没问题,但要理解它为什么安全:如果list_data为空,第一个条件为False,短路不会执行后面的表达式,不会报IndexError。
第三个坑:==判断浮点数。比如if result == 0.1这种,浮点精度问题会导致你永远匹配不上。正确做法是判断误差范围:if abs(result - 0.1) < 1e-6,或者用math.isclose。
3. for和while:循环不只是"重复执行",关键是可迭代对象
3.1 for循环的本质是迭代器
很多初学者把for循环理解为"重复执行N次",这个理解太浅了。Python的for循环本质上是从可迭代对象中逐个取出元素。for x in something,这个something可以是列表、元组、字符串、字典、集合、文件对象、生成器等一切实现了迭代协议的对象。
这个认知差异带来的实际影响是:你会更自然地写出针对"数据流"的循环。比如读取一个很大的日志文件:
with open("access.log", "r", encoding="utf-8") as f: for line in f: process(line)这里f本身是一个迭代器,逐行读取,不会一次性把整个文件加载进内存。如果你脑子里只有"循环N次"的模型,可能就会先readlines()读成列表再遍历——大文件时内存直接爆掉。
还有一个关键点:for循环次数不是提前算好的,而是取决于迭代对象的长度。边遍历边修改迭代对象的长度,就是经典的"坑",后面我会专门讲。
3.2 range、enumerate、zip:遍历时的三个好搭档
range不是列表,它是一个惰性序列。range(1000000)不会真的创建一百万个元素的列表,它只是保存了起点、终点和步长,遍历时按需生成。这在内存上非常省。range常见的三种用法:
range(5) # 0,1,2,3,4 range(2, 8) # 2,3,4,5,6,7 range(0, 10, 2) # 0,2,4,6,8enumerate解决的是"遍历时既要元素、又要索引"的场景:
for idx, item in enumerate(items, start=1): print(idx, item)第二个参数start我经常用——表格导出时想从1开始编号,这个参数就派上用场了。
zip解决的是"同时遍历多个列表"的场景:
for name, age in zip(names, ages): print(f"{name}今年{age}岁")注意zip按最短的序列截断,如果两个列表长度不一致,长的部分会被丢弃。如果希望按最长的走、缺失的补默认值,可以用itertools.zip_longest。
3.3 while循环:什么时候才应该用while
很多初学者分不清for和while。我用一句话概括:for适合"遍历已知集合",while适合"按条件反复执行,直到某个状态改变"。
比如写一个重试机制——请求接口失败就重试,直到成功或重试次数耗尽。这个场景用for就不合适,因为你不知道要重试几次;用while就很自然:
retries = 0 max_retries = 5 while retries < max_retries: try: result = fetch_data() break except NetworkError: retries += 1 time.sleep(2)这里break很关键,成功后立刻跳出循环。如果一直不成功,重试次数达到上限,循环正常结束。
while最大的风险是死循环。写while的时候一定要确认:循环体里必须有某个操作让条件最终变为False,否则程序永远出不来。我见过不少新手写:
while True: pass当然后来会用break跳出,但更常见的死循环是"条件变量忘更新"——比如condition在循环里一直没被重新赋值。排查死循环时,先看循环体内哪些变量会影响条件,再确认它们是否一定会在某次迭代中被改变。
3.4 边遍历边修改列表:经典翻车现场
这个坑我几乎每次提起都要感叹,因为太隐蔽了。需求是"从列表里删除所有偶数",新手很自然会写:
nums = [1, 2, 3, 4, 5, 6] for n in nums: if n % 2 == 0: nums.remove(n) print(nums) # 结果是 [1, 3, 5]???看起来删掉了所有偶数,但如果你有更多偶数连续出现,就会漏删。原因在于:for循环是按索引推进的,remove删掉元素后列表长度变了,后续元素的索引整体前移,循环却继续按原索引走,导致跳过了某些元素。
解决方案有三种:
第一,遍历副本,修改原列表:
for n in nums[:]: if n % 2 == 0: nums.remove(n)第二,用列表推导式重新生成:
nums = [n for n in nums if n % 2 != 0]第三,用while按索引手动控制:
i = 0 while i < len(nums): if nums[i] % 2 == 0: nums.pop(i) else: i += 1我平时最推荐列表推导式,简洁而且安全。记住一个原则:如果循环体里要修改正在遍历的容器,换一种方案,不要在遍历过程中做增删。
4. break、continue、else与嵌套循环:控制权的转移规则
4.1 break和continue只影响"当前一层"循环
这个知识点看起来简单,但嵌套循环里特别容易混淆。break让当前这层循环立即结束,continue跳过本轮剩余代码直接进入下一轮。注意措辞:"当前这层",不是"所有循环"。
看这个嵌套循环:
for i in range(3): for j in range(3): if j == 1: break print(i, j)内层循环在j == 1时break,但外层循环不受影响,继续跑下一个i。很多新手以为break能"跳出两层循环",实际上它只跳内层。这是面试里极高频的考查点,也是实际写代码时容易搞混的地方。
4.2 循环else子句:判断"循环是否正常结束"
Python的for和while后面可以跟一个else子句,它的语义是:循环没有被break打断,完整跑完,则执行else块。这个特性官方文档解释得比较隐晦,我用一个搜索场景说明。
假设你要在一个列表里找目标值,找到了就处理并break:
target = 5 for x in nums: if x == target: print("找到了") break else: print("没找到")如果循环里break了,else不执行;如果循环把列表遍历完都没break,说明没找到,执行else。这个写法比用一个found标志位干净得多。但注意,这个特性可读性对新手来说有点反直觉——很多人以为else是"循环结束后的收尾"。它确实是收尾,只是"不被打断的收尾"。如果觉得容易混淆,也可以继续用标志位,写代码首先考虑团队里别人能不能一眼看懂。
4.3 嵌套循环"跳出两层"的几种靠谱方案
有些语言支持带标签的break,比如Java的break outer。Python没有这种语法,常规做法有三种。
第一种,用标志位:
found = False for i in range(100): for j in range(100): if condition(i, j): found = True break if found: break第二种,把逻辑封装成函数,用return直接结束整个函数:
def search(): for i in range(100): for j in range(100): if condition(i, j): return i, j return None第三种,利用itertools.product拍平嵌套循环:
from itertools import product for i, j in product(range(100), range(100)): if condition(i, j): break我实际开发中最常用第二种,因为函数化之后不光解决了跳出问题,还让代码结构更清晰、可测试性更好。product方式适合只是为了遍历笛卡尔积的场景,省一层缩进。
5. 模式匹配match-case:Python 3.10带来的分支新写法
5.1 什么时候用match-case
Python 3.10开始引入了match-case(结构模式匹配),它跟其他语言的switch-case不一样,不是简单的等值匹配,而是能做结构解包和模式匹配。
先说最基础的等值匹配:
def handle_command(cmd): match cmd: case "start": start_server() case "stop": stop_server() case "restart": restart_server() case _: print("未知命令")这里_是通配符,相当于default。它比一长串if/elif可读性好一些,但纯等值匹配的场景,用字典查表也完全可以。我认为match-case真正发挥价值的地方在下一节。
5.2 结构模式匹配:处理复杂数据结构的利器
match-case能按结构匹配,比如我们现在有一个描述形状的数据,可能是("circle", radius),也可能是("rectangle", width, height),还可能是("point", x, y):
def area(shape): match shape: case ("circle", r): return 3.14159 * r * r case ("rectangle", w, h): return w * h case ("point", x, y): return 0 case _: raise ValueError("无法识别")这个写法最重要的一点是:它把"类型判断 + 解包"合在了一步。不用先isinstance判断类型,再一个一个取下标。如果是嵌套结构,还能继续匹配:
match data: case {"user": {"name": name, "is_admin": True}}: print(f"管理员{name}登录") case {"user": {"name": name}}: print(f"普通用户{name}登录")这里data是一个字典,case里直接写了字典的结构模式,匹配成功的同时把name解出来。这种写法在处理API返回的JSON数据时特别顺手。
还有守卫语法if:
match score: case s if s >= 90: print("优秀") case s if s >= 60: print("及格") case _: print("不及格")但注意,match-case的匹配顺序也是从上到下,第一个匹配成功就结束。守卫条件为False时会继续往下尝试。实际使用中,我建议把match-case用在结构匹配和字段解包场景,范围判断还是传统if更直观。
6. 异常处理也是流程控制:try/except/else/finally的完整语义
6.1 异常是"控制流的一部分",不是附属品
很多教材把异常单独拎出来讲,给人感觉它是"出错之后才用到的东西"。实操越多我越觉得,异常处理本质上是流程控制的一种——它描述的是"当某个步骤失败时,程序应该怎么走"。
举个最简单的例子,解析用户输入的整数:
try: num = int(user_input) except ValueError: print("输入的不是数字,请重新输入") num = 0这里的try/except其实就是个分支:正常情况下走try,异常情况下走except。它和if的区别在于,try块里的任何一行都可能触发跳转,而不是只在某个明确的条件判断点。理解这个模型,你写异常处理时会更有目的性:哪些操作可能失败?失败后应该走哪条路?是重试、降级、还是直接上报?
6.2 捕获粒度:不是所有的异常都适合except Exception
新手最容易犯的错误是try块里包太多东西,一个except Exception全部兜住。这么做在调试阶段省事,但上线后非常痛苦——真正的Bug被吞掉了,日志里看不到有用的信息。
我写代码时基本遵循两个原则:
第一,只捕获你预期会发生的异常。文件不存在用except FileNotFoundError,网络超时用except TimeoutError,类型转换失败用except ValueError。捕获特定异常,能让你的处理逻辑更有针对性。
第二,多个异常可以放在一个元组里统一处理:
try: process_data() except (ValueError, TypeError) as e: log_and_report(e)不同异常需要不同处理时可以分开写多个except子句。注意顺序:子类异常要写在父类前面。比如except OSError和except FileNotFoundError同时存在时,FileNotFoundError要放前面,否则它永远不会被匹配到。
6.3 else和finally:两个容易被忽视的子句
try/except后面可以跟else和finally,语法完整形态是try -> except -> else -> finally。
else在try块没有抛出异常时执行。这个设计有个好处是:可以区分"可能出错的代码"和"成功之后才执行的代码",避免在try里放太多内容、误捕获本不该捕获的异常。举个例子:
try: data = load_config() except FileNotFoundError: use_default_config() else: validate_config(data)如果validate_config放在try里,万一它抛了同样的FileNotFoundError就会被当成加载失败来处理,产生误判。放else里,就能明确"只有加载成功才做校验"。
finally是无条件执行的——无论try有没有异常、有没有break/return,都会执行。最典型的用途是资源清理:关闭文件、释放连接、恢复状态。
conn = create_connection() try: conn.query(sql) finally: conn.close()这个写法能保证连接一定会被关闭。注意,如果finally块里又抛了异常,它会覆盖try块原本的异常,所以finally里尽量不要放可能出错的复杂操作。
6.4 raise、自定义异常与"好代码的可读性"
有时候不是异常发生了才用except,你可以主动raise一个异常来改变控制流。比如参数校验失败时,与其返回一个特殊值让上层if判断,不如直接抛ValueError。特殊值容易让人忘了判断,异常则会强制调用方处理或显式向上传递。
自定义异常在项目变大之后很重要。比如你的业务里有很多校验逻辑,统一抛ValidationError,上层可以写一个except ValidationError统筹处理,而不用捕获一堆TypeError、ValueError。自定义异常很简单,继承Exception即可:
class ValidationError(Exception): """业务校验失败时抛出"""我还想提一个容易被忽略的实践:异常信息里要写清楚"发生了什么"以及"怎么修"。比如raise ValueError("age必须是非负整数,收到: {age}")。这个习惯会让你的队友(包括未来的你)省很多事。
7. 综合案例:用流程控制组合实现一个带重试的批量处理任务
前面讲了很多零散的知识点,这一节把它们组合起来,看一个实际任务怎么落地。假设我们要做的事是:读取一个CSV文件,里面有多条数据库连接配置,逐条尝试连接,连接成功后拉取数据,拉取失败自动重试,最后把整个处理过程的结果汇总。
7.1 任务拆解:先想清楚"程序有哪些出口"
写代码之前,我用30秒把这个任务可能会走的路径列出来:
- 文件不存在或格式错误:直接失败,不需要处理任何配置。
- 每行配置格式不对:跳过这一行,但不要影响后面。
- 连接失败:重试3次,每次间隔1秒。
- 连接成功但数据拉取异常:记录错误,继续下一行。
- 最终输出:每行配置的处理状态汇总。
有了这个出口清单,流程控制的骨架就出来了:外层用for遍历每行,内层用while做重试,用try/except处理各种异常,用计数器统计成功和失败。
7.2 完整代码与逐段说明
import csv import time from pathlib import Path class ConfigError(Exception): """配置格式异常""" def load_configs(filepath): """读取配置文件,返回配置列表,格式不对的抛ConfigError""" if not Path(filepath).exists(): raise FileNotFoundError(f"配置文件不存在: {filepath}") configs = [] with open(filepath, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: # 逐行校验,必填字段缺失直接当格式错误 if not all(k in row for k in ("host", "port", "query")): raise ConfigError(f"第行缺少必要字段: {row}") configs.append(row) return configs def fetch_with_retry(config, max_retries=3): """带重试的数据拉取,失败时抛出最后一次异常""" for attempt in range(max_retries): try: # 这里替换为真实的连接和查询逻辑 return pull_data(config["host"], int(config["port"]), config["query"]) except (ConnectionError, TimeoutError) as e: if attempt == max_retries - 1: raise e time.sleep(1) def main(): stats = {"success": 0, "failed": 0} try: configs = load_configs("configs.csv") except FileNotFoundError as e: print(f"启动失败: {e}") return except ConfigError as e: print(f"配置解析失败: {e}") return for config in configs: try: result = fetch_with_retry(config) except Exception as e: # 重试也失败了,记录并继续处理下一条 stats["failed"] += 1 print(f"处理失败: {config['host']}, 原因: {e}") continue # 拉取成功后,做后续处理 save_result(result) stats["success"] += 1 print(f"处理成功: {config['host']}") print(f"完成: 成功 {stats['success']} 条, 失败 {stats['failed']} 条") if __name__ == "__main__": main()从这个例子你可以看到流程控制是怎么组合的:
load_configs里,for遍历文件行,raise主动抛出异常控制"配置有问题就别继续了"。fetch_with_retry里,for循环加range实现"最多试几次",try/except捕获网络类异常,continue的逻辑用if attempt == max_retries - 1控制最后一次不再等待直接抛出。main里,外层for是主流程,try/except做单条失败兜底,continue让失败不影响后续行,统计计数器记录整体结果。
7.3 这个案例对思路的启发
我见过不少人写类似任务时,把所有逻辑塞在一个for循环里,try套if、if套try,最后几百行代码自己都看不懂。而如果先按上面的方式拆成"读取""重试""主流程"三层,每个函数的流程控制边界都很清晰,出问题也好定位。
核心原则就是四个字:异常隔离。每一层只处理自己该处理的异常:加载层负责文件解析异常,重试层负责网络类异常,主流程负责"单条彻底失败"的兜底。不要让异常无限嵌套传播,也别让一层捕获所有异常。
8. 排查流程控制问题时的自查清单
最后分享一份我自己整理的高频问题清单。这些问题是教初学者过程中反复遇到的,也经常出现在各种技术群里。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
IndentationError | 缩进层级不一致 | 检查Tab与空格混用,检查elif/else是否和if对齐 |
SyntaxError: invalid syntax | 上一行少了冒号或括号未闭合 | 检查if/for/while/def等语句结尾的冒号,检查括号匹配 |
| 条件分支执行了不该执行的分支 | 条件顺序写反,宽条件在前 | 检查elif顺序,缩小范围的条件放前面 |
| 死循环 | while条件一直被满足 | 确认循环体内有没有修改条件变量的操作 |
| 遍历结果跟预期不一致 | 边遍历边修改了容器 | 换成遍历副本或列表推导式 |
UnboundLocalError | 函数内对同名变量既读又写 | 检查变量是否在赋值前被引用,必要时用global或nonlocal声明 |
StopIteration | 对迭代器取元素取过头了 | 用for而不是手动next(),或捕获StopIteration |
except没执行 | 捕获的异常类型和实际抛出的不一致 | 打印实际异常类型,区分ValueError和TypeError等 |
finally里改变了返回值 | finally里有return或修改了外部状态 | finally只做清理,不要写业务逻辑和return |
这清单里我想重点展开两个。
第一个是UnboundLocalError,它特别阴。看代码:
count = 0 def increment(): count += 1运行时它会报UnboundLocalError: local variable 'count' referenced before assignment。原因是在函数内对count赋值,Python把count当成局部变量,而+=要先读再写,读的时候局部变量还没赋值。解决方法是函数里声明global count,或者把count作为参数传入传出。这个坑在流程控制里很常见——你想在某个分支里给一个外部变量累加计数,忘了声明就会炸。
第二个是关于"如何调试流程控制"。我的建议很简单:在关键分支处打日志,或者直接print。比如不确定某个分支是否执行,就在分支开头加一行print("进入了xxx分支")。有时候逻辑越复杂越不要硬想,跑一遍看输出,比人肉debug快得多。等你能熟练使用断点调试之后,再逐步替代print,但print永远是快速定位的第一工具。
还有一个习惯:写多分支判断时,先画一下"决策树"再写代码。你是要"先判断类型,再判断值",还是"先判断值范围,再判断边界"?这个顺序理清了,比任何调试技巧都管用。
9. 写流程控制的几条个人体会
代码写多了之后,我越来越觉得流程控制拼的不是语法熟练度,而是"对程序走向的预判能力"。同样的功能,不同人写出来,分支数量、嵌套深度、异常处理方式都会不一样。老手写的代码,分支少、嵌套浅、每个出口都有明确归宿;新手写的代码,层层嵌套,云雾缭绕。
分享一个我常用的"规则":如果某个分支的代码超过了5行,就考虑把它抽成函数。流程控制负责"怎么走",具体逻辑交给函数。这样哪怕分支再多,main函数也是从上到下一条清晰路径。
另外一个经验是:别怕多写几行,怕的是为了少写几行搞出绕逻辑。比如用match-case处理结构匹配很优雅,但如果团队里其他人对Python 3.10不熟,用写清楚注释的if/elif反而更稳。代码是给人读的,其次才是给机器跑的。
最后补一个小技巧:善用else和continue替代标志位。
# 不用标志位的写法 for item in items: if item.valid(): process(item) continue log_skip(item)这个写法把"有效则处理、无效则跳过记账"两种路径各自放在显眼的位置,比在循环底部判断"如果valid为False才skip"要直观。当然这只是个人偏好,重点是你形成自己稳定的风格,让读你代码的人能顺着你的思路走。
Python流程控制这块,内容不深,但织得很密。把if、for、while、try这四者的边界和连接搞清楚,你就能用最小的语法成本写出逻辑健壮的程序。后面写爬虫、写数据处理、写自动化脚本,这些基础能力会反复用到。希望这篇梳理能帮你少走点弯路。