
写Python也快十年了如果让我选一个最值得新手认真啃透的知识点我大概率会说流程控制而不是那些花里胡哨的框架。Python流程控制说穿了就一件事程序这条流水线接下来往哪走。条件判断走哪条分支、循环要不要继续、要不要提前退出都归它管。不管你是刚装好Python准备入门还是打算做爬虫、写量化策略、搞办公自动化代码里真正决定逻辑对错的就是这几个关键字。这篇内容适合所有正在学Python、尤其是对if和for停留在“会用但没吃透”阶段的读者。1. 先理清流程控制到底在管什么1.1 程序的三种基本走向正常人写代码很少一开始就考虑“控制流”这个抽象概念但拆开来看任何程序无非就是三种走向顺序执行、条件分支、循环重复。顺序执行最直白就是从上往下一条一条跑Python尤其如此因为它的缩进天然决定了执行顺序。条件分支是遇到了岔路口比如订单金额满300减50那就得用if去判断。循环重复则是让一段代码反复执行比如遍历一个列表里的所有商品、反复重试一个网络请求for和while就是干这个的。这三者组合起来就能表达现实中几乎所有逻辑。很多新手觉得“语法背下来了但写不出东西”其实是没把问题先翻译成这三种走向。拿到需求先想哪些步骤是有前置条件的哪些动作要重复做重复到什么时候停想清楚了写代码就是照抄流程控制关键字的事。1.2 Python里有哪些流程控制武器初看Python的流程控制关键字好像比C、Java精简不少。没有switch没有goto括号也不需要全靠if、elif、else、for、while、break、continue、pass这么几个词。但精简不代表弱后面你会发现Python用一个极其简洁的表面包住了很深的表达力。这里先给一张速查名单心里有个总谱条件分支if / elif / else循环结构for / while跳出与跳过break / continue占位空语句pass循环正常结束钩子elsefor和while都能搭配异常驱动的分支try / except / else / finally模式匹配match / casePython 3.10开始提供很多人只把前四个当流程控制其实循环else和异常分支也是流程控制的延伸。有个买基金的朋友说过一句很外行但很形象的话“代码和过日子一样最怕的就是走到哪算哪。”流程控制就是在给程序画地图告诉你哪个路口拐弯、哪条路循环走、哪个方向此路不通。1.3 先立一个“运行轨迹”的心智模型我在带新人的时候发现大家最喜欢问的问题是“为什么我这段代码结果不对”。先别急着查语法不如在纸上或者脑子里把程序执行轨迹画一遍。比如下面这段代码nums [1, 2, 3, 4, 5] total 0 for n in nums: if n % 2 0: total n print(total)执行轨迹是total初始为0循环第一次取n1if判断条件是假跳过第二次n2条件为真total变成2第三次n3为假跳过第四次n4total变成6第五次n5跳过。最后输出6。就这么一个简单的过程很多人跑不出来正确答案就是因为脑子里没有“每一步在哪个变量上做了什么改变”的概念。流程控制本质上是改变程序的运行轨迹所以调试的基础能力就是能顺着轨迹走一遍。后面所有章节讲的技巧都是为了让这条轨迹清晰可控。2. 条件判断if / elif / else 的高频用法2.1 基础结构怎么写得一眼就懂Python的if判断长这样score 78 if score 90: grade 优秀 elif score 60: grade 及格 else: grade 需努力 print(grade)几个细节我强调过无数次。第一Python不写大括号用缩进表示代码块一个块里的语句必须保持相同缩进量推荐4个空格。第二elif不是else if的简写语法糖那么简单它让你不用反复嵌套结构更平。第三分支从上往下匹配一旦某个条件为真后面的elif和else就不会再执行了所以条件的先后顺序非常重要。举个真实场景。QQ号黑白名单逻辑很容易写错如果先判断“用户存在”再判断“用户有权限”顺序反了就会误杀。再比如判断成绩等级一定从高到低写先把90分以上的捞走再处理60分以上的。要是你写成if score 60再写elif score 90那95分也会进“及格”因为第一个条件已经为真了。2.2 真假值判断比你想的更灵活大多数人以为if只会判断True和False但Python在这里有个很实用的特性任意对象都可以参与真假判断。0、0.0、None、空字符串、空列表、空字典、空元组、空集合都会被当作假其它值一律当真。这个特性用好了代码会非常清爽。比如检查一个列表是不是空的新手会写if len(items) 0: process(items)其实可以直接写if items: process(items)同理判断用户输入是否为空、字典里有没有键、对象是否为空容器都可以用“直接判断对象本身”的写法。我见过很多老代码还在用len(x) 0这种冗余判断不是错但读起来绕。这里要注意的是numpy数组里的判断容易遇到一个坑如果你对一个numpy数组直接写if arr:会抛“ValueError: The truth value of an array is ambiguous”因为数组里可能有多个元素没法转成一个真假。这是Python流程控制跟科学计算库交互时最常见的坑之一后面我会在问题章节展开。2.3 Boolean运算的三态and / or / not 的行为Python的not返回布尔值但and和or并不保证返回True或False它们返回的是参与运算的操作数本身。这是个非常迷人的设计。举个例子a 0 b 10 result a or b # result是10不是True原因是or在做“短路求值”先评估a如果a为真就直接返回a不再算b否则返回b。and反过来一遇到假就返回那个假值全真才返回最后一个真值。这个特性常用于给变量取默认值name input(请输入姓名:) or 匿名用户如果用户没输入input返回空字符串or就会返回右边的“匿名用户”。在爬虫、命令行工具里这种写法很多。但我也提醒一句返回操作数带来的潜在问题是你可能想拿布尔值结果拿到一个非布尔对象在if条件下没问题但拿去和True做比较就会出意外。真要布尔结果就写bool(a or b)或者用两个明确的比较表达式。2.4 不要滥用嵌套多考虑提前返回条件分支最容易写出“箭头形”代码一层套一层缩进越拉越深。代码缩进从4格变8格变12格最后你自己都看不清else和哪个if配对。一个容易上手的好习惯是提前返回。在函数里把不满足条件的情况先return掉让主流程留在平层级def process_order(order): if order is None: return if not order.is_paid: return if order.total 10000: return # 主流程继续处理 ...这样读起来像一段过滤管道而不是一个深不见底的嵌套结构。很多资深开发者的代码看起来平易近人就是学会了早点return、早点continue而不是把路径全都包进if里。3. 循环控制for 和 while 的选型与踩坑3.1 for 循环的本质是迭代器协议Python的for循环不是像C语言那样纯粹靠下标递增它的底层是迭代器协议。每次for循环开始时Python先调用内置函数iter()拿到一个迭代器然后循环体内不断调用next()取下一个元素一直到抛出StopIteration异常循环才自然结束。这一切对你不是透明的但它能帮你解释很多现象。最常见的例子有人觉得range(1, 5)生成了列表[1, 2, 3, 4, 5]其实range是一个惰性对象它只在你遍历时才产生数字在第n次迭代生成第n个数并不会一次性占满内存。遍历大量整数时range比生成列表省内存得多。同理for遍历文件对象是一行一行读而不是一次性全装入内存这也是爬虫处理大响应时常用for line in response.iter_lines()的原因。理解了迭代器就理解了为什么for循环里修改正在遍历的列表会出问题。Python在for循环内部维护了一个索引如果你在循环体里append、remove元素列表长度和那个索引对不上了最常见的表现是跳过某个元素或者多遍历一次。一句忠告不要在for循环中修改被遍历的容器。真要边遍历边筛选就复制一份或者先收集需要删除的元素循环结束后再一起删。3.2 while 循环适用的场景和几个禁忌for适合遍历已知集合while适合“不知道要循环多少次只知道什么时候停”的场景。比如重试某个接口调用最多试5次每次失败等2秒这种就很适合用while实现retry 0 while retry 5: try: response fetch_data() break except TimeoutError: retry 1 time.sleep(2)我用while最大的体会是循环条件必须写在代码的同一个视野内并且一定要有能改变条件的语句。最常见的死循环就是while True里面忘记写break或者while count 10里面没有更新count。每次看到现象是程序卡住、CPU跑满第一反应就是查while条件有没有更新。另外while和for的选择不是强制的但for更安全。如果你的循环只是遍历一个列表那for优先如果需要复杂终止条件再退到while。这个顺序能帮你避开一大半死循环问题。3.3 range、enumerate、zip 的搭配用法流程控制里最容易被忽略的是那些帮你管理循环迭代的辅助函数。range的三个参数range(start, stop, step)注意stop是开区间也就是不包括stop本身。要生成倒序序列可以range(10, 0, -1)。enumerate在使用场景上非常实用。比如我需要同时拿到元素和下标新手可能写for i in range(len(items)): item items[i]而更优雅的写法是for i, item in enumerate(items, start1): print(i, item)start1可以让序号从1开始写报表、输出日志时经常用到。zip则适合并行遍历两个列表它按最短的那个列表截断。如果你希望按最长的走就用itertools.zip_longest并指定fillvalue。这三个工具配合for基本能覆盖日常90%的循环需求。我经常看到网上有人喷Python的for不够强其实不是for不强而是很多人不知道还有enumerate和zip这种内置功能。4. 循环辅助语句break、continue、pass 与循环 else4.1 break 与 continue 的边界与替代方案break的作用是彻底结束当前循环不管循环条件是否还满足直接跳到循环后面的代码。continue的作用是结束本次迭代回到循环的判断处开始下一轮。这两个词入门容易难在什么时候用。很多人习惯用一堆if else嵌套来模拟break和continue结果代码越来越绕。其实你完全可以让流程控制词大胆出现。比如搜索第一个满足条件的元素并停止for item in items: if item.is_available(): result item break else: result None print(result)这个例子里如果循环正常跑完都没找到就会走else把result设为None。这个“循环else”的搭配救了我很多次它省去了设置标志位的麻烦。你不需要提前定义一个found False再去判断Python的else子句天然表达“没有被break掉”的意思。另外一个经验如果循环体内条件判断特别多而且经常要提前结束不妨考虑把逻辑抽成函数用return提前退出而不是层层设标志位。return和break的相似之处都是“从当前流程里跳出去”区别是return会同时结束整个函数。4.2 pass 不是摆设pass是一个空语句它告诉你“这里语法上必须有一个语句但我不打算做任何事”。新手经常觉得pass没用或者和continue搞混。区别在于continue是“跳过这次的剩余代码继续下一圈”pass是“什么都不做占个位置继续往下走”。常见的用途有几个。第一个是写代码骨架时先占位def download_file(url): pass # 稍后实现第二个是捕获异常但不处理try: dangerous_call() except Exception: pass # 暂时忽略这个写法要小心裸except和pass组合会让异常被悄无声息吞掉排错时特别难受。我后来都会至少加一个日志或者注释哪怕只是print一下也比纯pass强。4.3 循环 else 子句一个隐藏多年的好特性我第一次接触到循环else时非常震惊因为很多教材根本不提它。它解决的问题是“如何判断循环是否正常结束”这个判断在搜索、校验场景里太常见了。它的行为是如果循环完整执行完一次break都没遇到那么else块会被执行一旦执行了breakelse就被跳过。一个快速判断质数的例子对比一下就明白num 29 for i in range(2, int(num ** 0.5) 1): if num % i 0: print(f{num}不是质数) break else: print(f{num}是质数)如果写成标志位的版本要多好几行可读性还差。循环else这种写法的好处是把“找不到”“没遇到异常”这类情况放到循环后面用一个分支处理代码更加线性。踩过的坑就是else会跟着for的结尾缩进如果是和if的else混在一起稍不留意缩进错了逻辑就完全变了。建议刚开始用的朋友在这个结构上多打印验证几次。5. 进阶玩法推导式、match-case 与异常流程控制5.1 用推导式压缩循环代码如果循环的目的是从某个可迭代对象生成一个新的列表或字典那可以用推导式。它不只是写法简化很多时候性能也更好因为底层有专门的优化路径。列表推导式squares [x ** 2 for x in range(10) if x % 2 0]等价于squares [] for x in range(10): if x % 2 0: squares.append(x ** 2)推导式前面的表达式是“对每个元素做什么”后面跟for指定来源再后面可以跟if做过滤。字典推导式、集合推导式也都支持squares_dict {x: x ** 2 for x in range(5)} unique_chars {c for c in python if c.isalpha()}不过凡事有度。如果循环体内逻辑复杂比如有多个条件分支、有异常处理、有嵌套循环硬写成推导式反而难读。判断标准很简单一眼能看懂就上推导式一眼看不懂就写普通循环。代码是给人读的不是用来炫技的。5.2 match-case 的模式匹配Python 3.10Python 3.10引入的match-case我第一次看到时觉得只是换了个花样的switch后来写多了才发现它远不止如此。它可以做字面量匹配command list match command: case start: start_server() case stop: stop_server() case list: show_list() case _: print(未知命令)它还能按结构匹配。比如处理一个用户数据结构想匹配“管理员且状态为active”match user: case {role: admin, status: active}: grant_admin_panel() case {role: admin}: wait_for_activation() case _: show_public_page()这个在处理字典、元组、类对象时比一堆if user.get(role) ...要清楚。还有guard语句可以加条件case {role: role, score: score} if score 80: print(f{role} 高分)match-case的引入把一部分流程控制从if中解放了出来尤其是解析命令、处理JSON结构、按状态机流转的场景代码的声明性高很多。不过它毕竟是较新的语法如果项目要兼容Python 3.9或更低版本就不建议用。这种兼容性问题在给别人写脚本、部署到老服务器时要特别注意。5.3 try / except / else / finally 的流程控制视角很多人觉得异常处理和流程控制是两码事其实从“程序下一步去哪”这个角度看异常处理就是流程控制的另一种形态。基本结构是try: value int(user_input) except ValueError: print(输入的不是数字) else: print(f转换结果是{value}) finally: print(无论成不成功都会执行)我在实际开发里最常用到的组合是try except elsetry里只放可能出错的代码else里放“只有成功才执行的代码”。这样避免了把成功逻辑也塞进try里、最后分不清异常来自哪一步的问题。另一个点是不要用裸except。裸except会捕获包括KeyboardInterrupt在内的所有异常程序想用CtrlC中断都做不到非常糟糕。至少也要写成except Exception或者更精确到类型。finally通常和return一起使用时需要特别注意。如果try里有returnfinally还是会执行而且如果finally里也有return它会覆盖try里的return。这个坑我踩过一次排查了很久才发现是finally里误写了一个返回值。6. 常见问题与排查技巧实录6.1 死循环排查思路与预防办法死循环是流程控制里翻车率最高的一个问题。现象就是程序启动后一直转、不退出、CPU占用直接拉满。排查思路我一般按下面顺序走先看有没有whilewhile后面有没有可能永远为真的条件。再看循环体内有没有更新判断条件的语句。最后看break是不是被continue跳过或者被if挡在外面了。经典案例是i 0 while i 10: if i % 2 0: continue print(i) i 1这段代码里当i是偶数时continue直接把i 1跳过了i永远不会变成奇数也永远不会超过10死循环。这种坑的隐蔽性很高因为你不是没写i 1而是它在某种分支里没有执行到。预防办法很简单我习惯在循环体开头统一更新条件变量而不是把更新分散在末尾或者嵌套分支里。还有一个辅助手段调试时临时加个打印计数器看循环到底卡在哪一次。6.2 循环内修改集合的安全做法遍历列表时删除元素是每个Python开发者迟早都会踩的坑。比如nums [1, 2, 3, 4, 5] for n in nums: if n % 2 0: nums.remove(n) print(nums)结果是[1, 3, 5]吗不一定实际可能是[1, 3, 4, 5]因为遍历和删除同时发生时列表索引发生了漂移有个元素被“跳过去”了。更危险的是遍历字典时直接删除键d {a: 1, b: 2, c: 3} for key in d: if d[key] 2: del d[key]这会直接抛RuntimeError报错信息是dictionary changed size during iteration。安全的做法是先收集再删除to_delete [key for key, value in d.items() if value 2] for key in to_delete: del d[key]或者直接创建一个新字典/新列表new_nums [n for n in nums if n % 2 ! 0]说实话现在就比较推荐用推导式替代这种“边遍历边筛选”场景意图清晰不碰原始容器省心很多。6.3 作用域和嵌套循环的认知误区Python没有块级作用域这是个经常让人意外的点。在if、for、while里定义的变量循环结束后依然可以被外部访问for i in range(5): last i print(i) # 输出4C语言开发者第一次遇到这种事会非常不适应因为C里面for循环里的变量出了循环就销毁了。Python则保留了这个变量。这带来一个隐含问题如果你在循环里用了变量名x而且这个x和外部某个变量重名循环就会覆盖掉外部变量的值。另一个常见问题在嵌套循环里break只能跳出最内层循环。很多人想在两层循环里“找到一个结果就全部退出”于是写break结果发现外层还在跑。 这需要加标志位或者把双层循环抽成函数用return或者抛异常打断。我最常用的是抽函数加return语义最清楚。嵌套循环的另一个坑是性能。如果内层循环做了太重的操作比如反复创建对象、反复读写文件里面的耗时会被循环层数放大。可以考虑把能提出来算的东西放到循环外这也是优化流程控制的一种思路。6.4 一张表格看清常见报错与解决办法实操中积累的报错经验和应对策略这里统一整理一下方便你排查时快速定位。报错/现象常见原因解决办法死循环、程序不退出while条件未更新、continue跳过更新在循环开头更新条件变量加调试打印IndexError: list index out of range循环内下标超出当前列表长度改用for item in list或先检查lenRuntimeError: dictionary changed size during iteration遍历字典时增删键先收集键循环结束后再删ValueError: truth value of an array is ambiguous对numpy数组直接if判断用arr.any()或arr.all()明确意图UnboundLocalError函数内使用与全局变量同名的变量并赋值明确global声明或避免同名循环else不执行循环内提前break或else缩进层级错误检查break位置与else对齐方式continue跳过了增量语句循环末尾的i 1被continue跳过把增量语句前移到continue之前这些报错里很多不是语法错误而是流程控制结构写得不符合预期。排错时一定要有“当前执行轨迹在哪”的意识而不是死盯着某一行看。我的习惯是在关键节点加print输出变量值跑一遍看轨迹比单纯阅读代码快得多。6.5 调试流程控制的几个实用小技巧到这里顺便分享几个我日常调试流程控制的小技巧。第一善用打印标记。在循环开始和结束的位置各打一行可以看到循环执行情况和退出原因用缩进输出还能模拟出嵌套层级。第二用断言做约束。如果循环里有不变量比如total必须递增可以assert total prev_total一旦违背立刻暴露。第三把复杂循环拆成小函数。循环越短越容易控制把内层逻辑抽出去主循环就剩三五行流程一眼到底。第四善用itertools。islic、permutations这些工具能处理很多手工循环容易出错的迭代模式自己写循环反而容易索引混乱。这些技巧谈不上高深但对新手排查流程控制问题特别管用。很多时候看不懂代码不是智商问题是手上没有趁手的调试工具。写代码说到底就是在控制流程。从我带新人的经验看凡是卡在语法层面的大多是对“程序下一步会走到哪”没有画面感。如果你能把每段流程控制在脑子里像放电影一样过一遍变量的每一步变化都清晰可见那多数疑难报错都难不住你。我自己到现在写复杂循环也仍然保持“先在草稿纸上跑一遍轨迹、再落代码”的习惯。这个习惯不花多少时间但能省下大把调试的功夫。流程控制学得好不好就看你能不能把脑子里那套乱糟糟的逻辑翻译成一条清晰、确定、可预测的代码路线。