你翻开鱼书的那一刻,最先看到的往往不是神经网络,而是一段Python代码。我带过不少想入深度学习的朋友,大家拿到那本封面上印着一条鱼的书后,第一件事都是补Python基础。有意思的是,真正挡住大家的不是求导、不是矩阵运算,恰恰是最不起眼的if和for语句。
为什么?因为鱼书里所有算法实现,靠的都是这两种语句在驱动数据流动——什么时候停止迭代,什么时候切换路径,什么时候清洗异常值。if和for就是Python代码的交通灯,你写不写得对,直接决定后面几十页代码能不能跑起来。这篇文章不绕弯子,把if和for掰开揉碎:基础语法、组合用法、常见坑,最后还有可以直接抄走的小项目。零基础也好,已经能写几百行代码但总在细节上翻车也好,都值得花十分钟看完。
1. 从"鱼书"进度看:为什么控制流才是你真正需要的第一个知识点
很多新手以为学Python要先背函数、先搞懂类,其实翻开鱼书前几章就知道,代码的基本形态就是顺序执行加几个循环和判断。你完全可以不背任何高级特性,但一定要把if和for练成条件反射。
1.1 Python代码分两类:一类算值,一类决定走向
Python里大部分语句都在"算出结果":x = 2 * 3算完就过去了,程序继续往下走。但if和for不一样,它们会改变程序的"流向"——可以跳过某段代码,也可以反复执行某段代码。这个区别就是控制流的本质。
x = 6 if x > 5: print("x大于5")这段代码里,x = 6只是给变量赋值,真正有意思的是if x > 5这一行:它像在路口看了一眼信号灯,决定要不要进入下面的代码块。鱼书里的网络参数初始化、梯度检查、批次划分,说到底都是这种"先判断、再选择路径"的写法。
1.2 思路先于语法:训练自己"读代码"的习惯
我见过太多人学if和for时一直在抄代码,抄完就忘。我的建议是反过来:先学会在脑子里"纸面执行"一条代码。拿到一段小代码,拿支笔在草稿纸上画一下,每个if进去会走哪条分支,每个for循环内部会循环几次,把结果写下来再去跑程序对比。这样做几次之后你会发现,语法本身不需要刻意背,代码读多了自然就记住了。
读鱼书的时候也是一样,别急着把整段实现复制到编辑器里跑,先试着回答三个问题:这个循环在遍历什么?每个数据进来会命中哪个if分支?循环退出之后,累积下来的变量里存的是什么值?这三个问题能答清楚,你基本就真的读懂了。
2. if语句解剖:从单一判断到多分支决策
if语句的核心不是"判断"这个动作,而是"路径选择"。同一个条件,写法不同,结果可能完全不一样。
2.1 最基础的if与Python的真值规则
最朴素的if长这样:
if 条件: 执行这里条件可以是任何表达式,甚至可以直接是一个变量。Python会先把条件转成布尔值(True或False),再决定要不要执行。这里有个新手容易忽略的点:Python判断一个值"为真"不只是看它是不是True,空字符串、空列表、0、None,统统都会被当成False。
| 值 | 布尔结果 | 说明 |
|---|---|---|
0、0.0 | False | 数字0被当作假 |
"" | False | 空字符串 |
[]、()"、"{} | False | 空容器 |
None | False | 空值 |
| 其他非空值 | True | 比如1、"a"、[1] |
这个表看起来简单,实际特别有用。比如你想判断某个列表是不是空,不需要写if len(my_list) == 0,直接写if my_list:就够了。反过来,if not my_list:就表示"列表为空"。这种写法在鱼书的数据预处理里经常出现,用来跳过空样本。
2.2 elif和else的顺序匹配:条件不是并行而是从上往下
只有一个条件的if很简单,麻烦的是多分支。比如给成绩打分:
score = 85 if score >= 90: grade = "优秀" elif score >= 60: grade = "及格" else: grade = "不及格"elif和else的作用是"前面没命中,再看下一个"。Python执行时会从上往下逐个匹配,一旦命中某个分支,后面的条件就全都不会再看了。很多人踩坑就是没意识到顺序问题,比如把条件换成这样:
if score >= 60: grade = "及格" elif score >= 90: grade = "优秀" else: grade = "不及格"85分一下就走进了第一个分支,第二个分支永远没有机会命中,90分以上的学生也会被误判成"及格"。所以写多分支时,一定要把范围更窄、更严格的条件放在前面。
2.3 嵌套if的缩进与"判断组合"的改写
if里面再套if,就是嵌套判断。比如判断一个体温数据是否属于发烧:
t = 38.5 if 35 <= t <= 42: if t > 37.3: print("体温偏高") else: print("体温正常")嵌套本身没问题,但层级一多,缩进就会成为负担,一个Tab对错位置代码就废了。很多情况下,嵌套可以改写为用and连接多个条件,逻辑更平直:
if 35 <= t <= 42 and t > 37.3: print("体温偏高")and的意思是两个条件同时满足才进入分支,正好替代一层内层if。注意,嵌套模式和and改写并不总是完全等价,如果你在内部else里还有更细的逻辑分支,那就别强行压平。我的经验是:超过两层的嵌套,先想想是不是可以用and、or或提前结束逻辑来简化,实在简化不了就保留嵌套,但一定要统一缩进规格,全项目都用4个空格。
3. for循环:把"可迭代对象"这个词搞懂就够了
如果说if是地图上的岔路口,那for就是一条流水线。流水线旁边站着一个工人,负责从传送带上逐个取下零件处理——这个"取下零件"的动作在Python里叫迭代。
3.1 for的本质是逐个取出元素,而不是控制i的加减
很多学过C语言的人刚转Python时会问:for循环里的i在哪自增?答案是Python没有这个机制,它只会做一件事:把一个可迭代对象里的元素逐个取出来,塞进循环变量。
for item in [10, 20, 30]: print(item)这里item会自动依次等于10、20、30,不需要你手动往下拨。这个"取出"的动作对不同类型的对象有不同表现,我第一次带学员时总会单独列一张表对比:
| 可迭代对象 | 每次取出的内容 | 示例输出 |
|---|---|---|
[1, 2, 3] | 列表元素 | 1、2、3 |
"abc" | 单个字符 | a、b、c |
{"a": 1, "b": 2} | 字典的键 | a、b |
range(3) | 0到2的整数 | 0、1、2 |
字典那条特别容易忘。默认遍历字典拿到的是键,如果你想同时拿键和值,要写for key, value in scores.items():。鱼书里经常用到以字典存储超参数,这个细节会反复出现。
3.2 range的三个参数:从"要循环几次"到"控制步长"
range是Python最常用的"数字生成器",很多人只记得range(5)生成0到4,其实它有三个参数:
for i in range(5): # 0, 1, 2, 3, 4 print(i) for i in range(1, 6): # 1, 2, 3, 4, 5 print(i) for i in range(0, 10, 2): # 0, 2, 4, 6, 8 print(i)range(起始, 结束, 步长)的核心思路是:起始包含,结束不包含,步长按正负方向移动。步长可以是负数,比如range(5, 0, -1)会生成5、4、3、2、1。
初学者最常用到range的场景是索引遍历:
scores = [88, 92, 76] for i in range(len(scores)): print(i, scores[i])这里的思路是"我要的是位置,不是元素本身"。当你需要同时使用下标和元素时,直接遍历scores拿不到下标,所以得靠range。但这个问题其实有更Pythonic的解法,接着往下看。
3.3 enumerate和zip:让循环同时拿到"序号"和"关联值"
手动用range(len(...))取下标不是不行,但代码一长就容易下标写错。更稳的写法是enumerate,它会在遍历元素的同时自动帮你编号:
names = ["A同学", "B同学", "C同学"] for idx, name in enumerate(names, start=1): print(idx, name)start=1的意思是编号从1开始,默认从0开始。这么做的好处是你不再需要手写idx += 1,也就少了一个出错点。
另一个常用的是zip,它能把多个列表按位置"拉链式"地组合起来:
names = ["A同学", "B同学"] scores = [88, 92] for name, score in zip(names, scores): print(name, score)zip的循环里一次能拿两个变量,非常适合"一份数据对应一份标签"的场景。鱼书里最常见的数据处理就是把输入和标签配对遍历,zip这种写法比用下标i去两个列表里分别取值要直观得多。
4. 把if和for组合起来:筛选、计数与提前退出
if和for单独用都是入门水平,真正的日常代码几乎都是两者套在一起。组合的方式就那么几种,学会了就可以应对绝大多数场景。
4.1 循环里套if:先过滤再处理的标准动作
最常用的写法是"循环遍历每一个元素,再用if判断这个元素要不要处理"。比如有一批体温测量数据,里面混着异常值-999,我们要把有效体温收集起来:
raw_data = [36.5, 37.2, 39.1, -999, 36.8, 41.5] valid = [] for t in raw_data: if t == -999: continue if 35 <= t <= 42: valid.append(t) print(valid)这段代码里出现了两个if:第一个用于跳过脏数据(continue后面会讲),第二个才是真正的范围过滤。处理顺序是"先排除非法值,再判断合法值",这个顺序比把两个条件揉成一个复杂表达式要清晰得多。
这类"过滤-收集"模式几乎可以套用到任何榜单、成绩单、传感器数据的清洗场景中。鱼书里处理图像数据时也会做类似的事:读出一批样本,把损坏的、尺寸不对的先跳过,剩下的才进入训练流程。
4.2 break、continue和for-else的实际分工
循环内的if除了做筛选,还经常用来改变循环本身的行为。三个关键词要分清:
continue:跳过本次循环的剩余部分,直接进入下一次迭代break:立即跳出整个循环for-else:循环正常结束(没有break命中)时,执行else块
举个例子,在1到9里找第一个能被7整除的数:
for i in range(1, 10): if i % 7 == 0: print("找到了", i) break else: print("没有符合条件的数")找到7之后就break跳出,不浪费时间继续迭代。如果没有break,for循环走完后会进入else块,告诉你没找到。for-else这个特性很多Python老手都容易忽略,但它特别适合"搜索后报告结果"的场景,比用一个flag变量记录"有没有找到"要干净得多。
4.3 列表推导式:if和for的"语法糖"版本
看多了代码你会发现,前面那种"循环加if然后append"的模式实在太常见,于是Python给了个简化写法——列表推导式:
raw_data = [36.5, 37.2, 39.1, -999, 36.8, 41.5] valid = [t for t in raw_data if 35 <= t <= 42] print(valid)这行代码做的事和上面那个for循环一模一样。阅读顺序是:先看for t in raw_data,再看if 35 <= t <= 42,最后看最前面的t是"计算出来的元素"。整体意思就是"从raw_data里遍历t,如果t满足条件,就把t放进新列表"。
如果你还想在放入前做点变换,比如把度数转成华氏度,可以直接写[t * 1.8 + 32 for t in raw_data if ...]。不过我的忠告是:推导式虽然简洁,但条件超过两个、表达式再一复杂,可读性会迅速变差。那时候老老实实写for加if,比硬压缩成一行要好。代码是给别人读的,其次才是给机器跑的。
5. 跟着鱼书学Python时最容易踩的五个坑
我在带人学Python的过程中,把新手在if和for上踩的坑总结成了五个高频问题,几乎每个学员都至少中过两个。
5.1 缩进用Tab还是空格:别赌编辑器默认设置
Python用缩进来表示代码块,同一个文件里混用Tab和空格就等着报错。最常见的场景是:从网页复制了一段代码,编辑器默认用Tab,你手敲的部分用了空格,然后报IndentationError,甚至代码能运行但逻辑是错的。
我的建议是不要纠结哪种更好,直接约定项目统一用4个空格。绝大多数现代编辑器都支持"把Tab转成空格"的选项,IDE里一般叫"Insert spaces"或"Tab Size: 4"。设置好之后,缩进问题基本消失。别小看这个问题,鱼书里大段代码你手动重敲的时候,一半报错都跟缩进有关。
5.2 遍历列表时顺手删除元素:索引偏移的经典翻车
你以为在for循环里直接删除列表元素没什么,实际结果可能和想象完全不一样:
nums = [1, 2, 3, 4, 5] for n in nums: if n < 3: nums.remove(n) print(nums) # 期望 [3, 4, 5]运行后你会发现,剩下的很可能是[2, 3, 4, 5]。原因是for循环内部是按位置往下走的,你删掉1之后,2挪到了位置0,循环指针却已经滑到了位置1,于是2就被跳过了。这种"跳元素"问题非常隐蔽,数据量一大更难发现。
解决办法很直接:不要在遍历一个列表的同时修改它,而是构建一个新列表。
nums = [1, 2, 3, 4, 5] new_nums = [n for n in nums if n >= 3] print(new_nums)这个原则同样适用于字典:遍历字典时往里加键或删键,都很容易触发运行时错误。鱼书里处理数据集时,几乎永远是把"筛选后的数据"存到新变量里,很少原地删除,这也是当前工程上的默认做法。
5.3 循环结束后的变量"残留"与命名影响
Python的for循环结束后,循环变量并不会被销毁,它仍然保留着最后一次迭代的值:
for i in range(3): pass print(i) # 输出 2看着无害,但如果后面的代码刚好也用i当作某个遍历索引,就可能在你不注意时把之前的值带进去,产生隐蔽的逻辑错误。更麻烦的是在函数内,for循环里的变量会覆盖同名外层变量,循环结束之后外层的值就回不来了。
给循环变量起有含义的名字是最省事的预防手段。比如遍历成绩就用score,遍历批次就用batch,不要永远用i、j。鱼书里循环变量通常很长,比如for i in range(batch_size),你跟着写多了会逐渐养成好习惯。
5.4 判断相等时误用==和is
新手写相反了:判断数字、字符串相等时,该用==却写成了is;判断是否为空时,该用is None却写成了== None。在Python里,==比的是"值是否相等",is比的是"是不是同一个对象"。
a = 42 b = 42 print(a == b) # True,值相等 print(a is b) # 可能True也可能False,取决于解释器机制对小整数,Python解释器有时会做缓存,导致is碰巧返回True,给人"这样写没问题"的错觉。但一旦换成大数、字符串、列表,is的结果就可能让你怀疑人生。
我的建议:除了和None比较可以用if x is None,其他所有值比较都用==。这不是风格问题,而是避免把"碰巧成立"当成"掌握得牢靠"的典型例子。
5.5 用while代替for时漏更新计数器:死循环离你只有一行
鱼书的代码以for为主,但偶尔你会自己写while,或者在别人的代码里看到while。while的问题在于没有内置的"下一步"机制,一切靠你自己更新计数变量。
count = 0 scores = [88, 92, 76] while count < len(scores): print(scores[count]) # count += 1 ← 忘了这行,程序就永远停不下来一旦忘写count += 1,count永远为0,条件永远成立,程序就死循环了。轻则卡住终端,重则让电脑风扇狂转。我的习惯是:能用for表达的循环绝不用while,只有在条件本身就依赖某个变量变化、且不适合"遍历一个可迭代对象"时才用while。真要写while,先把计数器更新的那一行写在循环体最前面,再写业务逻辑。
6. 综合实战:用if和for写一个可运行的小程序
说再多不如跑一遍。我设计了一个鱼书风格的迷你任务,把前面所有内容串起来,你可以直接复制到本地跑。
6.1 需求拆解:从成绩统计看数据处理的基本姿势
假设有一份学生成绩列表,需要统计三件事:及格人数(>=60分)、不及格人数、平均分,并且给每个学生标上"优秀/及格/不及格"的等第。这个任务和数据预处理特别像——遍历所有样本、逐个判断、累积统计指标。鱼书里对训练数据做的基本就是这个模式的变体。
先写伪代码理清思路:
对每个分数: 累加总分到 total 如果分数 >= 60: 及格人数 + 1 否则: 不及格人数 + 1 循环结束后: 平均分 = total / 总人数伪代码能写出来,说明思路已经清楚,剩下的就是把每一步翻译成Python。
6.2 完整代码与逐段解释
下面这段代码可以在Python 3里直接运行:
scores = [88, 57, 92, 63, 45, 76, 100, 39] pass_count = 0 fail_count = 0 total = 0 for score in scores: if score >= 60: pass_count += 1 else: fail_count += 1 total += score average = total / len(scores) print(f"及格人数:{pass_count}") print(f"不及格人数:{fail_count}") print(f"平均分:{average:.1f}")第一行的列表是原始数据,三个变量分别记录及格人数、不及格人数和总分。for循环遍历每个分数,if负责分流,total += score无论是否及格都会执行,因为它在if-else结构之外。最后用len(scores)拿到总人数算平均值。
如果想顺便输出每个学生的等第,再用一个循环:
for idx, score in enumerate(scores, start=1): if score >= 90: grade = "优秀" elif score >= 60: grade = "及格" else: grade = "不及格" print(f"第{idx}个学生:{score}分 -> {grade}")这里用到了enumerate拿序号、多分支if判断等第、f-string格式化输出,一份数据一次搞定。
6.3 测试运行、结果验证与延伸玩法
运行之后,你会得到类似这样的控制台输出:
及格人数:5 不及格人数:3 平均分:70.0 第1个学生:88分 -> 及格 第2个学生:57分 -> 不及格 第3个学生:92分 -> 优秀 第4个学生:63分 -> 及格 第5个学生:45分 -> 不及格 第6个学生:76分 -> 及格 第7个学生:100分 -> 优秀 第8个学生:39分 -> 不及格及格5人、不及格3人,平均分正好70,和手工验算对得上。等你跑通这版,可以试着扩展:把-1作为缺失分数,用if跳过它;把列表换成从文件里读出来的数据;或者把统计结果封装成一个函数,输入任意分数列表都返回统计字典。这些扩展看起来简单,但正好覆盖了if和for组合的绝大多数用法。
如果你也正在啃鱼书,我的建议是别急着往后翻。花一个晚上把前面这些if和for的写法练熟,写几个像上面这样的小程序跑通,再回去看深度学习代码,你会发现自己读代码的速度至少快一倍。学到后面你会发现,费曼那句话说得一点不假:真正掌握一个东西的标准,是你能不能把它教给别人。而if和for,就是你教给别人的第一个Python故事。