多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python结构语句核心详解:分支、循环与控制流实战

Python结构语句核心详解:分支、循环与控制流实战 如果你跟我一样刚开始学 Python 的时候只背了一堆变量、列表、字典的定义但一到自己写点什么就卡在原地那问题多半出在结构语句上。Python 结构语句说白了就是三件事按什么条件走哪条路、同一件事怎么重复做、怎么在重复过程中及时跳出去。程序一旦加上了这些控制逻辑就不再是从上到下一条路走到黑的命令列表而是真正拥有决策能力的自动化流程。本系列前面已经铺垫了变量、数据类型和运算符这篇就把 if、for、while、break、continue 这些结构语句掰开揉碎讲清楚底层逻辑再配合经典倒推实战题帮你把这块硬骨头彻底啃下来。1. 结构语句到底在解决什么问题把顺序执行升级成按需决策1.1 顺序、分支、循环结构化编程的三种基本结构很多人写代码的时候会有一种错觉觉得代码就是一行接一行地执行直到碰到结束。其实一个稍微像样的程序极少是纯直线结构的。想象一下你做一个西红柿炒鸡蛋先放油热了之后放蛋炒好了盛出来再放西红柿最后混合翻几下。这是顺序结构。但如果你打开冰箱发现没鸡蛋了你就要去超市买这就是个分支结构。如果你要炒的是十盘菜每一盘都要重复洗菜-切菜-炒菜这就是循环结构。早在几十年前计算机科学家就总结出一个结论任何复杂的程序逻辑都可以拆解成顺序、分支、循环这三种基本结构的组合。Python 里的结构语句就是这三类结构的具体实现。顺序结构代码按照书写顺序依次执行这个不需要额外关键词Python 默认就是从上往下跑。分支结构核心是 if / elif / else让程序根据条件真假选择不同的执行路径。循环结构核心是 for 和 while让一段代码反复执行指定次数或直到条件不再满足。理解了这三类结构你就理解了编程的主干。后面的列表推导、异常处理、生成器、装饰器这些花活儿本质上都是在这个主干之上长出来的枝叶。1.2 Python 把缩进当作语法结构层次的天然表达这里有个其他语言老手很容易忽略的点Python 用缩进表示代码块。在 C 系语言里代码块靠大括号缩进只是起美化作用在 Python 里缩进本身就是语法的一部分。score 85 if score 90: print(优秀) else: print(再接再厉)if 下面那行 print 前面必须有缩进否则程序直接报 IndentationError。这个设计好不好用见仁见智但它带来的直接好处是结构语句的层级关系一目了然你永远不会出现找另一半大括号找了半天的情况。对新手来说这其实是个保护机制逼着你把代码层次写清楚。注意同一个代码块内部缩进必须保持一致。我习惯统一用 4 个空格不用 Tab。因为 Tab 和空格混用时编辑器里看起来是对齐的但解释器会直接报错这种坑排查起来极其痛苦。1.3 掌握结构语句的最终目标能用代码表达规则结构语句学习的好坏不是看你背了多少语法而是看你能否把一段自然语言描述的规则翻译成 Python 代码。比如如果用户输入的不是数字就提示重新输入给一个列表里的每个数都乘 2这类规则无处不在。你在项目里写的每一个功能几乎都是这些规则的叠加。所以在开始逐条拆解语法之前建议你先在心里建立一个预期每一个结构语句都是在回答一个具体的操作问题。if 回答什么时候做for 回答对每个元素做什么while 回答什么时候做到头break 回答什么时候提前结束continue 回答哪些情况跳过不做。掌握结构语句的最终标准就是拿到一个需求能立刻判断该用哪个结构以及它们怎么组合。2. 条件分支 if/elif/else看懂 Python 的判真规则比记语法更重要2.1 基础语法与成绩等级的例子if 语句是结构语句里最常用的一类语法本身非常简单if 条件1: # 条件1为真时执行 elif 条件2: # 条件1为假且条件2为真时执行 else: # 上面的条件都不满足时执行先说两个刚上手最容易搞错的细节。第一条件后面必须跟冒号这个冒号经常被漏掉。第二条件内部判断的是这个表达式的真假值而不是这个条件成立与否这种模糊的表述。举个经典例子把百分制成绩转成等级score 86 if score 90: grade A elif score 80: grade B elif score 70: grade C elif score 60: grade D else: grade F print(grade)这个例子看起来简单但它体现了一个很重要的设计原则elif 是有顺序的解释器从上往下逐个判断一旦某个条件为真就跳过后面所有分支。所以写多路分支时条件的覆盖范围要逻辑清晰不能相互交叉模糊。很多人把条件的顺序写反比如先判断 score 60 再判断 score 90那 95 分就会先落入第一个分支等级变成 D这显然不对。2.2 会被当成 False 的隐藏值0、None、空容器Python 的条件判断有一个和 C/Java 截然不同的特性不是只有布尔类型才能做条件。任何对象都可以直接放在 if 后面做真假判断解释器会隐式地把它转换成布尔值。这个机制用得好代码能精简不少用得不好就是一堆隐晦的 bug 来源。在布尔上下文中以下值会被判定为 False常量 False 本身数值 0、0.0None空字符串 、空列表 []、空字典 {}、空元组 ()、空集合 set()其他所有值包括负数、非空字符串、非空容器、自定义对象都被判定为 True。这个特性的实用价值体现在哪里举个例子从接口返回的数据经常可能缺失data get_user_info() # 可能返回 None也可能返回字典 if data is not None: print(data[name])更符合 Python 习惯的写法是if data: print(data[name])因为如果 data 是 None 或者空字典都会被判为 False自然不会进入分支。所以这里的逻辑是安全的。这种写法在真实项目里非常常见一旦你习惯了再看回 if data ! None and data ! {} 这种写法就会觉得非常啰嗦。警示用 if data: 做空值判断的前提是你明确知道空和错误之间的区别。有的场景里空字典是合法结果而 None 才是异常两者不能混为一谈。例如接口返回的查询结果本身就是空列表你不希望走失败分支。这时候应该用 if data is not None 来精确判断而不是 if data。2.3 and 与 or 的短路求值既是坑也是优化手段条件分支里通常要组合多个条件于是就要用逻辑运算符 and、or、not。大多数教程只告诉你它们表示与或非但很少有人主动讲短路求值这是结构语句里一个很隐蔽却很重要的行为。Python 的逻辑运算遵循短路规则计算 a and b 时如果 a 已经是假那么结果一定为假b 根本不会被计算计算 a or b 时如果 a 已经是真那么结果一定为真b 也不会被计算。这个规则的最典型应用是防止程序在无效数据上崩溃if user is not None and user.age 18: print(允许访问)如果 user 是 None前一个条件是假整个 and 表达式直接短路返回假后面的 user.age 不会执行也就不会抛出 AttributeError。如果你把这个条件顺序反过来写成 user.age 18 and user is not None那 user 为 None 时第一句就会崩溃。这种由短路带来的顺序依赖是新手写条件判断时最容易踩的暗坑之一。同样的逻辑or 短路可以用来设置默认值name input(请输入你的昵称: ) display_name name or 匿名用户如果用户输入了空字符串name 为假or 表达式继续计算右侧于是 display_name 变成 匿名用户。如果用户输入了内容整条表达式的值就是 name。这是 Python 里非常地道的默认值写法。不过要提醒一句短路特性用起来很爽但不要把多个 and/or 叠成一行长表达式那会严重损害可读性。条件太复杂时宁可拆成多行或者先把条件结果存进变量再判断。3. for 与 while 的正确选择弄懂迭代原理和条件循环的边界3.1 for 不是计次循环而是遍历可迭代对象刚接触 Python 的人很容易拿着 C 语言的思路来理解 for 循环总觉得 for 就是个计数器。Python 的 for 本质上不是从 1 数到 10而是遍历一个可迭代对象中的每一个元素。你给它什么它就把里面的元素挨个取出来给你用。最简单的例子fruits [苹果, 香蕉, 橘子] for fruit in fruits: print(fruit)这个循环做的事情是从 fruits 里取出第一个元素赋值给变量 fruit执行循环体然后取第二个元素再执行循环体直到列表掏空。所以 for 循环的第一步是搞清楚你要遍历的对象是什么。遍历字典时默认拿到的是键info {name: 小明, age: 18, city: 北京} for key in info: print(key) # 拿到键如果想同时拿到键和值用 items() 方法for key, value in info.items(): print(key, value)遍历字符串时拿到的是一个个字符for ch in Python: print(ch)很多初学者会把 for 循环的循环体变量名随便写比如 for i in fruits然后在循环体里又搞不清楚 i 到底是列表还是元素。我个人的建议是变量名尽量反映它代表的内容。遍历列表就写 for fruit in fruits遍历字典键值对就写 for key, value in info.items()这样代码一读就通不会绕晕自己。3.2 range控制遍历范围的三个参数计数式的循环在 Python 里通过 range 对象实现。range 有三种写法写法含义例子结果range(stop)从 0 开始到 stop 之前结束range(5)0, 1, 2, 3, 4range(start, stop)从 start 开始到 stop 之前结束range(2, 7)2, 3, 4, 5, 6range(start, stop, step)按 step 步长递增/递减range(1, 10, 2)1, 3, 5, 7, 9注意两点。第一range 的结束值是不包含的也就是左闭右开。第二step 可以为负数所以可以倒着遍历for i in range(10, 0, -1): print(i) # 10, 9, 8, ..., 1如果需要同时拿到索引和元素本身enumerate 函数比手动用 range(len(...)) 更简洁names [张三, 李四, 王五] for index, name in enumerate(names, start1): print(index, name)这里的 start1 是把索引从 1 开始编号如果你的逻辑里编号必须从 0 开始就用默认值。这类小细节看似不起眼但在实际编码里非常省事也能避免一堆索引越界问题。3.3 while 适合条件驱动的场景while 循环的语法是while 条件: # 条件为真时反复执行和 for 循环不同while 不关心遍历对象的个数它只关心一个条件。只要条件为真就一遍遍执行循环体直到条件变成假。所以 while 最典型的应用场景是不知道要循环多少次只知道什么时候该停。场景一等待用户输入合法数据。age None while age is None: user_input input(请输入年龄: ) if user_input.isdigit(): age int(user_input) else: print(输入不合法请重新输入)用户可能输入一次就成功也可能连续输错五回这个循环次数靠 for 是没法预先确定的用 while 自然得多。场景二累加求和直到超过某个阈值。total 0 n 1 while total 100: total n n 1 print(total, n)这段代码不断把 n 加进来直到 total 达到 100 才停。你光是盯着代码很难一眼看出最后 n 是多少但这种条件到了就停的写法恰好就是现实世界很多规则的样子。切记一点while 循环里要确保条件最终会变成假否则就是死循环。最典型的是忘记更新循环变量比如 while total 100 内部一直没给 total 增值程序就一直空转下去。写 while 之前先问自己一句循环体里哪个变量在变化何时会让条件不成立3.4 循环加 else没人讲过却很有用的子句Python 的 for 和 while 后面都可以跟一个 else 子句这个语法真实项目里用的人不多但非常好用。它的语义是如果循环正常结束就执行 else 块如果循环是被 break 中断的就不执行 else 块。一个常见场景是判断一个数是不是质数num 29 for i in range(2, num): if num % i 0: print(f{num} 不是质数) break else: print(f{num} 是质数)如果 29 能被某个 i 整除break 跳出循环else 不执行如果从 2 到 28 都不能整除循环正常结束else 执行说明是质数。这个写法比用标志变量 is_prime 再在最后判断要清爽得多。这个语法还有一个好处是语义贴合实际场景循环体正常走完全程说明没有发生特殊情况所以做一些收尾工作。理解了这个语义你会在很多场景下想起它。要注意的是第一次看到 for...else 的人往往会理解成else 是循环结束后必然执行忽略了 break 的例外这里建议你自己动手多测几个例子加深印象。4. break、continue、pass 的语义辨析从控制流程到工程占位4.1 break找到第一个结果就立刻收工break 用于提前终止整个循环不再管后面还有多少元素没遍历。最典型的场景线性查找。假设你在一串姓名列表里找第一个姓王的人names [赵大, 钱二, 孙三, 王四, 周五] for name in names: if name.startswith(王): print(f找到了: {name}) break程序找到王四之后立刻离开循环后面的周五不会再被判断。为什么需要 break两个理由一是性能如果数据量很大找到目标再继续空转是浪费二是语义我已经拿到我要的结果了不用继续找。这个过程和现实中的找到了就停手逻辑完全一致。嵌套循环里的 break 要特别注意它只终止当前这一层循环。比如两层的 for 循环里内层循环的 break 只会跳出内层外层还会继续跑。如果你需要跳出多层循环常见的做法是设置一个标志变量或者把逻辑抽成一个函数后用 return 跳出def find_target(matrix, target): for row_idx, row in enumerate(matrix): for col_idx, val in enumerate(row): if val target: return row_idx, col_idx return None这也是我在真实项目里遇到嵌套查找时更推荐的做法用函数包裹逻辑直接 return比各种标志变量清爽得多。4.2 continue跳过本轮继续下一轮continue 的作用是跳过当前这一轮循环的剩余代码直接进入下一轮迭代。它不是结束整个循环只是这一轮我不干了。举个数据处理场景把一个列表里的所有偶数保留原值所有奇数变为 0。numbers [3, 8, 5, 12, 7] for i in range(len(numbers)): if numbers[i] % 2 0: continue numbers[i] 0 print(numbers) # [0, 8, 0, 12, 0]当 numbers[i] 是偶数时continue 让循环体跳过后面的赋值语句直接进入 i1。当 numbers[i] 是奇数时正常执行赋值把它改成 0。continue 也经常用来做白名单/黑名单过滤。比如在清洗数据时遇到某些不合规的字段直接跳过不处理即可。需要提醒的是continue 别用得太密集。如果一个循环体里有七八处 continue阅读代码的人会很难理清执行路径。这时候更合理的做法是把循环体拆成函数或者反过来用 if 包裹更简单的主逻辑。4.3 pass占位符在工程里的真实价值pass 是 Python 里一个比较反直觉的语句字面意思是什么都不做。注意它不是跳过也不是注释而是一个合法的语句用来维持语法结构。最常见的用途是在还没想好怎么实现的时候先把代码骨架写出来def send_email(user): # TODO: 等待邮件服务接入 pass def calculate_discount(total): if total 1000: pass else: return total这样代码能够正常跑起来也不会有语法错误。如果你用空函数体Python 会直接报错因为函数体不能为空。有些人会用 docstring 来占位例如 def send_email(user): 发送邮件但 pass 表示的语义更明确这地方先留空之后再填。在 if 的分支里pass 也常用作暂时不处理该情况的占位。和其他语言对比一下C 系语言里空代码块是合法的但 Python 不行所以 pass 就是 Python 世界里用来表示空操作的官方工具。5. 嵌套结构和变量作用域缩进深度决定逻辑层级5.1 分支嵌套实例两级登录验证结构语句可以互相嵌套if 里面有 if循环里面有循环循环里还可以有分支。嵌套的缩进层级直观反映逻辑的从属关系。拿一个简化版的登录验证来看username input(用户名: ) password input(密码: ) if username admin: if password 123456: print(登录成功) else: print(密码错误) else: print(用户不存在)这个逻辑的核心是先验证用户名再验证密码。外层 if 处理用户名内层 if 在用户名正确的前提下继续验证密码。这种嵌套关系通过缩进一眼就能看清。但要说明的是嵌套不是越深越好。三层以上的嵌套会让代码变得难读这时候可以考虑用卫语句来提前返回把嵌套拍平。上面的例子如果把密码错误和用户不存在都提出来逻辑层次会更清晰。经验分享我自己写条件的时候会尽量避免超过三层的缩进。一旦觉得嵌套太深就停下来想想能不能用前面的逻辑运算符号把条件合并或者能不能提取一个函数。嵌套本身不是错但可维护性是硬指标。5.2 双层循环实例九九乘法表的顺序逻辑循环嵌套最经典的例子是打印九九乘法表。这个例子的价值在于理解外层循环一次内层循环完一轮这层关系for i in range(1, 10): for j in range(1, i 1): print(f{j} x {i} {i * j}, end ) print()跑一下这个代码1 x 1 1 1 x 2 2 2 x 2 4 1 x 3 3 2 x 3 6 3 x 3 9 ...你仔细观察就会发现i 每取一个值内层 j 都会从 1 一直取到 i把这一整行的乘法都打印完然后 print() 换行再进入 i 的下一个值。如果你把循环的先后顺序颠倒打印出来的表会完全不同理解外层与内层的执行节奏是掌握嵌套循环的关键。5.3 Python 没有块级作用域循环变量在外层依然可见这一点是很多从其他语言转过来的开发者容易惊讶的在 C 系语言里for 循环里声明的变量默认在循环结束后就销毁了但 Python 里循环变量是一种名为函数级作用域的东西循环体里绑定的变量循环结束后依然存在于当前作用域。for i in range(5): pass print(i) # 输出 4而不是报错这个特性的实际影响是什么一方面你可以在循环结束后继续访问 i 的最终值有时确实有用另一方面如果你在函数里写了一个循环循环变量可能意外覆盖掉同名的外层变量造成不易发现的逻辑问题。比如index 100 for index in range(3): pass print(index) # 2原来的 100 被覆盖了一般真实项目不会刻意依赖这个特性但你心里要有数。如果你不想让循环变量泄露影响其他代码比起时刻记得清理更稳妥的做法是给循环变量取一个不会和外层冲突的名字或者把这段逻辑封装成函数。6. 更 Pythonic 的结构语句替代写法列表推导式与条件表达式6.1 列表推导式for 加 append 的一行版本学完基础结构语句之后你马上就会遇到一个常见的提醒Python 的 for 循环不是万能的有些场景用列表推导式更简洁高效。列表推导式本质上是把遍历元素、筛选元素、生成新列表这三件事压缩成一行# 普通写法 squares [] for i in range(1, 11): squares.append(i ** 2) # 列表推导式 squares [i ** 2 for i in range(1, 11)]两者效果一样但推导式代码更短、更直接。加筛选条件也非常自然# 只取偶数平方 even_squares [i ** 2 for i in range(1, 11) if i % 2 0]这行代码读起来的思路是遍历 1 到 10如果 i 是偶数就计算 i 的平方放进列表。列表推导式的执行顺序恰好和英语阅读顺序一致先 for再 if再表达式。很多新手在推导式里把 if 放在 for 前面就会立刻报语法错误所以记清楚这个顺序很关键。需要注意列表推导式适合相对简单的逻辑。如果 for 循环体内还要写多行复杂处理那明显用普通循环更合适。为了追求简短而牺牲可读性是得不偿失的。6.2 条件表达式给简单 if/else 瘦身条件表达式也叫三元运算符是 Python 里把简单分支压缩到一行的方法age 20 status 成年 if age 18 else 未成年对应的普通写法是if age 18: status 成年 else: status 未成年条件表达式的可读性其实是个争议话题但多数有经验的开发者会同意只有分支赋值结果非常简单时才值得用。如果你在一个条件表达式里又嵌套另一个条件表达式代码马上变得很难读这种情况请老老实实写 if/else。6.3 嵌套推导式的适用边界列表推导式还可以支持嵌套循环比如把一个二维列表展平matrix [[1, 2, 3], [4, 5, 6], [7, 8, 9]] flat [num for row in matrix for num in row] # [1, 2, 3, 4, 5, 6, 7, 8, 9]这个推导式的执行顺序依旧是先外后内for row in matrix 先取出每一行然后 for num in row 把行里的每个元素取出来最后返回 num。这个写法和下面这个普通循环等价flat [] for row in matrix: for num in row: flat.append(num)在我看来这种嵌套推导式在单向循环里还勉强能读如果再加上 if 条件一行可能长到需要横向滚动。遇到这种情况我宁可写回普通嵌套循环。把结构语句用得简洁是一个方向但绝不是唯一目标。7. 综合实战用结构语句破解李白打酒经典倒推题7.1 先看清题意正推为什么困难倒推为什么可行前面把 if、for、while、break、continue、pass 这些结构语句都过了一遍现在做一个把知识串起来的实战题。这道题在 Python 学习圈里流传很广题目是这样的李白街上走提壶去买酒。遇店加一倍见花喝一斗。三遇店和花喝光壶中酒。试问壶中原有多少斗酒翻译成人话李白一开始壶里有若干斗酒。他遇到酒店酒量翻倍遇到花喝掉一斗。他一共遇到三次店、三次花而且顺序是店、花、店、花、店、花这样交替。最后一次见花之后壶里刚好喝光。问最初壶里有多少酒。如果顺着时间顺序正推设初始酒量为 x经过遇店加一倍变成 2x再见花喝一斗变成 2x - 1然后又是店变成 4x - 2再花变成 4x - 3……推到最后列一个方程也能解。用 Python 做这道题更让人拍案叫绝的解法是倒推。倒推的思路是从最后一次操作结束时的状态出发逆向反推上一步。最后一次是见花喝一斗后酒量归零那么见花前壶里必然是 1 斗。再往前一步是遇店加一倍店前酒量加一倍后等于 1 斗那店前应该是 0.5 斗。继续往前推就能一路推出初始酒量。7.2 用 while 循环倒推逐步验证每一步把倒推过程转成 Python 代码用 while 循环来模拟逆向遍历六步操作。先把倒退过程当成一组逆操作wine 0 # 最后喝光壶中酒为 0 # 从最后一步开始往前倒推。 # 正序是店 - 花 - 店 - 花 - 店 - 花 # 倒序依次是花 - 店 - 花 - 店 - 花 - 店 # 逆操作见花喝一斗的逆操作是加一斗遇店加一倍的逆操作是减半。 for operation in [花, 店, 花, 店, 花, 店]: if operation 花: wine 1 # 逆着喝一斗所以要加一斗 else: wine / 2 # 逆着加一倍所以要除以 2 print(wine)输出结果是 0.875也就是 7/8 斗初始酒量。这个数字对不对我们可以顺着验证一下初始 0.875 斗遇店翻倍成 1.75遇花喝掉 1 斗剩 0.75再遇店翻倍成 1.5遇花剩 0.5再遇店翻倍成 1.0遇花喝 1 斗正好归零。如果想把整个流程更直观地打印出来可以加一个正向验证的 for 循环wine 7 / 8 steps [店, 花, 店, 花, 店, 花] print(f初始酒量: {wine}) for step in steps: if step 店: wine * 2 print(f遇店加一倍: {wine}) else: wine - 1 print(f见花喝一斗: {wine})这样运行下来每一步的状态都清清楚楚最后一步正好归零。这道题的精妙之处在于它完整地展示了结构语句里三类核心能力的配合倒推的 while 思路对应循环条件设计每一步的判断对应分支结构最终验证对应正向循环和 if 组合。能把这种问题写明白说明你对结构语句的掌握已经是能用的程度了。7.3 扩展思考遇到类似问题时怎么建立逆向思路李白打酒这类题目换个角度看本质上是一个状态变化题。正推是顺着自然时间流看状态变化倒推则是从终点往起点看。编程里很多问题都有这种从结果反推原因的思路比如调试时从报错现场往上找源头比如回溯算法里从目标状态往回搜索。学会了倒推你就多了一种解决复杂结构问题的武器。我的建议是学完结构语句之后不要急于去背框架、搞花活而是拿这种小题目练手。把 if 的条件设计、while 的终止条件、for 的遍历顺序、break 的提前退出这些基本单元像积木一样拼在一起解决实际问题。等你看到三遇店和花能立刻想到倒推看到找第一个满足条件元素能立刻想到 break 加查找看到不知道循环次数只知道停止条件能立刻想到 while那结构语句这关才算真正过了。后续你再看列表推导式、异常处理、函数和类都会顺畅很多。
返回列表