Python第3次作业复盘:列表推导式、类型转换与环境排错
2026/9/9 4:05:41 网站建设 项目流程

说句实话,刚看到“python第3次作业”这个标题时,我自己都觉得平平无奇,但如果你跟我一样正处于刚学完条件判断和for循环、准备做第一次综合练习的阶段,就会明白这份作业的分量。第一次作业是print一个变量,第二次是写if和for,第三次开始真正碰列表、类型转换,有时还要从网络接口读一次数据——从这一步往后,光靠上课听已经不够,查安装教程、调Python环境、反复看懂报错信息,都会变成日常操作。

这篇文章不是我帮你把作业代跑一遍,而是一份完整的复盘记录,适合正在学Python、恰好也做到第3次作业附近的人。我会把拿到题目后的拆解思路、每道题背后涉及的语法点、运行过程中踩到的环境坑、以及最后沉淀下来的几个习惯全部写出来。哪怕你做的题目跟我的不完全一样,这一套处理问题的方式也是可以直接复用的。

1. 拿到题目先别写代码,我把“第3次作业”拆成了三件事

我这次拿到的题目看起来并不复杂,总共三个模块:基础语法小测验、列表奇偶拆分、读取一个返回JSON数据的公开测试接口并保存结果。第一次看到时觉得“就这?”但真正动手后才发现,题目越简单,越容易在细节上翻车。所以我没有立刻打开编辑器,而是先花十分钟把任务拆开。

1.1 先区分:这次的作业和之前有什么不一样

前两次作业本质上是“单点练习”:print、变量赋值、if判断、for循环,每一题都只考一个知识点。第三次作业最大的变化是开始把多个知识点串在一起。比如列表拆分的题,表面考的是for和if,实际还藏着“变量作用域”的问题;读取接口的题,表面考的是requests,实际还涉及“编码转换”和“数据结构解析”。

我把自己的任务整理成了这样一张表:

作业模块表面考点真正要练的能力
基础语法小测验int、float、str之间的类型转换理解数据在内存里的真实形态,而不是背API
列表奇偶拆分for循环、if判断、列表推导式搞清楚哪些逻辑适合一句表达式,哪些用循环更清晰
读取JSON接口requests、json解析把网络返回的文本变成可处理的结构化数据

做完这个表格之后,我意识到说“python第3次作业”难度不高的人,多半是把考点停留在了第一列。真正拉开差距的其实是第二列,也就是每个知识点背后的“为什么”。带着这个视角再去看题,感觉就不一样了。

1.2 环境体检:用10分钟排除最没价值的报错

作业开始前,我先做了个“环境体检”,这一步看起来跟作业没关系,却能帮你排除掉最没价值的报错源。我当时在终端里依次确认了三件事:

python --version pip --version where python # Windows系统;macOS/Linux用 which python

python命令能看到版本,说明解释器装好了;pip能输出版本,说明包管理工具可用;where能打印路径,说明没有“多条Python混在一起”的隐患。这三步执行完,再打开代码编辑器(我用的是VS Code)去跑作业,环境问题基本能挡掉80%。

提示:如果用的是VS Code,打开作业文件夹后,按Ctrl+Shift+P输入“Python: Select Interpreter”,确认你选中的解释器和你终端里python --version显示的是同一个。这一步多花十秒,能避免后来各种“模块装不上”的灵异事件。

环境没问题,后面做题才不会被无关因素干扰。接下来就进入正题。

2. 奇偶切分那一题,真正坑我的不是逻辑而是变量作用域

这次作业里有一道经典的列表题:给一个包含整数的列表nums,要求把奇数放到odd列表、偶数放到even列表,并分别输出两个列表的和。这类题我在网上看过无数种写法,初学者最自然的就是循环加判断:

nums = [3, 8, 1, 6, 5, 2, 7, 4] odd = [] even = [] for number in nums: if number % 2 == 1: odd.append(number) else: even.append(number) print(odd) # [3, 1, 5, 7] print(even) # [8, 6, 2, 4] print(sum(odd), sum(even)) # 16 20

这段代码没有任何问题,逻辑完全正确。但我后来把代码优化成了列表推导式之后,衍生出了一个值得大讲特讲的疑问,也正是这个疑问让我对Python的作用域规则有了更深的理解。

2.1 循环写法为什么要改成语义更强的推导式

列表推导式看起来像“高级写法”,但它真正的价值不是为了炫技,而是把“创建列表”这个语义压缩成一行,让读代码的人一眼就知道你打算干什么:

odd = [number for number in nums if number % 2 == 1] even = [number for number in nums if number % 2 == 0]

这两行输出和上面的for循环完全一样。对比两层写法会发现,循环的好处是可以在循环体里做不止一件事,比如同时往两个列表里append;而推导式的好处是结构封闭、可读性强。所以在作业里,我用循环写了初始版本,又尝试用推导式优化,最后保留了两种版本并加注释说明各自的适用场景。

这个习惯后来帮我避免了很多没必要的争论:代码不是越短越好,而是意图越清楚越好。

2.2 推导式里的变量,不会顺手改掉外部同名变量

真正把我绊倒的是一个看似偏门的问题。假设我已经在代码前面定义了一个number = 100,后面再用推导式[number for number in range(3)],等推导式跑完,外部那个number还是100吗?

答案是:还是100。

出于好奇,我当时在作业旁边写了个小实验:

number = 100 values = [number for number in range(3)] print(number) # 100,不是2 print(values) # [0, 1, 2]

但如果你把同样的逻辑写成普通for循环:

number = 100 for number in range(3): pass print(number) # 2,被循环变量覆盖了

这就是推导式和循环的本质区别:Python 3的列表推导式有自己独立的作用域,循环变量不会泄漏到外部;而普通for循环没有独立作用域,循环结束之后,循环变量会留在当前命名空间里。

我当时就因为这个特性,在作业里额外写了半页实验笔记。很多初学教程不会主动讲这个细节,但如果你以后会把数据清洗、特征提取这种代码封装成函数,就会知道“函数内局部变量和列表推导式内的变量互不干扰”是多么省心的一件事。这也是为什么我建议做第3次作业时,不要只满足于跑出正确答案,多问一句“为什么”,比多刷十道题更值。

2.3 类型转换题:int、float、str之间的常见误用

作业里另一个基础题是类型转换。题目要求接收用户输入的一个带小数的字符串,例如"3.14",把它转成浮点数后乘以2再输出整数结果。我一开始觉得太简单了,直接写了这个:

number = input("请输入一个小数:") result = int(number) * 2 print(result)

一运行,如果输入"3.14"就报错,报错信息是ValueError: invalid literal for int() with base 10: '3.14'。原因很简单:int()做的事是把字符串解析成整数,它不认识小数点。想处理带小数点的文本,必须先转float

number = input("请输入一个小数:") result = int(float(number) * 2) print(result)

这段代码先执行float(number)"3.14"变成浮点数3.14,乘2得到6.28,再用int()截断成6。注意这里不是四舍五入,是直接向零取整,如果你想四舍五入,需要用到round(6.28)

这个例子看起来普通,但它很好地解释了“类型转换”的真实含义:字符串和数字在内存里的形态完全不同,"3.14"只是三个字符,Python不经过解析就不可能拿它做数学运算。做题时多看一步“它现在是什么类型、我要什么类型、能从当前类型直接转过去吗”,以后碰到数据清洗里的脏数据就不会害怕了。

3. 头一回用requests读接口,我连续改了三个小问题

第三次作业里安排了一道读取公开JSON接口的题,当时我的第一反应是“这也太超前了,还没教爬虫就要我们请求接口?”但真正做下来才发现,这道题并不是想让我们学爬虫,而是想让我们体验一下“程序从外部获取数据”的完整链路。

题目要求很简单:请求一个公开的JSON占位接口,获取返回数据里的title字段,打印出来,并把完整结果保存到本地文件。我用的是公开测试接口https://jsonplaceholder.typicode.com/todos/1,这类专供学习使用的接口没有真实用户数据,也不包含任何敏感信息,适合当作业练手。

3.1 为什么第三次作业突然开始碰网络数据

我一直觉得这道题的目的不是“学会爬虫”,而是让我们接触真实项目中常见的“数据从哪来”的问题。前面的作业,所有数据都是自己在代码里手写的,说实话有点“温室环境”。等到第三次作业,数据突然变成别人服务器返回的JSON文本,你必须去解析、清洗、保存,整个流程的复杂度瞬间上来了。

我完成的第一个可用版本长这样:

import requests import json url = "https://jsonplaceholder.typicode.com/todos/1" headers = { "User-Agent": "python-homework/0.1 (learning python)" } response = requests.get(url, headers=headers, timeout=10) print("状态码:", response.status_code) if response.status_code == 200: data = response.json() print("任务标题:", data["title"]) with open("todo_result.json", "w", encoding="utf-8") as file: json.dump(data, file, ensure_ascii=False, indent=2)

这段代码的流程是:用requests.get请求接口,检查返回状态码是不是200,如果是,就调用response.json()把服务器返回的JSON文本解析成Python字典,然后打印标题,最后用json.dump保存到文件。如果作业做到这里就能正常运行,那你已经跑通了一条“网络请求—解析—落盘”的完整链路。

3.2 第一次运行,卡在编码和响应头这两个地方

但这版代码不是一遍就跑通的,我前后改了三个问题。第一个问题是运行之后没有任何输出,因为接口返回了非200状态码。检查后才发现,我不小心把URL拼错了一个字母。这个教训特别基础,但也特别值得记住:程序没有反应时,先把URL复制到浏览器里访问一下,看看它到底能不能通,这能排除掉大量“看起来是代码问题,实际是网络/地址问题”的情况。

第二个问题是编码。我把完整响应打印出来时,发现中文变成了乱码。原因是很多接口在响应头里没有明确声明charset,requests库这时会按自己默认的编码去解码,结果自然就错了。解决办法很直接,拿到响应之后手动指定编码:

response.encoding = "utf-8"

第三个问题是保存文件时,Windows的默认编码不是UTF-8,所以我在open()里明确写了encoding="utf-8",否则后面在别的系统上打开文件就容易出现“UnicodeDecodeError”。再配合json.dumpensure_ascii=False,保存下来的中文才是可读的,而不是\uXXXX这样的转义序列。

3.3 我在作业里最终交付的版本(含逐行说明)

最后我把三个问题都改完,交付版本里还额外做了一个小函数,方便从不同ID的接口获取数据:

import requests import json from pathlib import Path def fetch_todo(todo_id: int) -> dict: url = f"https://jsonplaceholder.typicode.com/todos/{todo_id}" headers = {"User-Agent": "python-homework/0.1 (learning python)"} response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 状态码不是200就会抛异常 return response.json() def main(): for current_id in range(1, 4): data = fetch_todo(current_id) print(data["id"], data["title"]) output_dir = Path("output") output_dir.mkdir(exist_ok=True) output_file = output_dir / f"todo_{current_id}.json" with open(output_file, "w", encoding="utf-8") as file: json.dump(data, file, ensure_ascii=False, indent=2) if __name__ == "__main__": main()

这个版本加了.venv那样的工程化习惯,比如用raise_for_status()主动抛出异常,而不是靠手动判断状态码;比如用pathlib.Path管理目录和文件;比如用if __name__ == "__main__"把入口单独隔离出来。这些习惯在作业阶段看起来“过度设计”,但到了第四次作业开始做大一点的项目时,你会感谢自己提前养成了它们。

需要强调一点:如果以后你对“爬虫”这个方向感兴趣,请一定要远离任何非公开数据接口、登录后才能访问的内容,以及任何可能涉及隐私的请求。学习阶段使用专门的测试接口,既安全又合规。

4. ModuleNotFoundError 不是代码问题,是环境里“多版本Python”打架

做完上面那题之后,我把代码保存成homework3.py,在终端里运行:

python homework3.py

结果迎面撞上一堵墙:ModuleNotFoundError: No module named 'requests'

我当时的第一反应是“那我装一下呗”,于是执行:

pip install requests

终端显示安装成功,完美。可当我再次运行python homework3.py时,还是同样的报错。那一下午我就在“运行报错—装包—再运行—还报错”的循环里来回转,差点把键盘拍烂。

4.1 一次典型的装包事故现场

后来仔细排查才发现,罪魁祸首是电脑上不止一个Python解释器。我的电脑之前装过某个软件,那个软件顺带装了一个内置的Python;后来我为了做作业又单独装了新版Python。这样系统里就有两套解释器,它们各自有自己的site-packages目录,也就是第三方包的存放位置。

我在终端里执行pip install requests时,系统默认把包装进了旧解释器的目录;而VS Code或终端里python命令指向的,却是新解释器。两边各说各话,自然找不到包。这种现象在多系统、多版本共存的环境里太常见了,尤其是做过其他开发、装过科学计算工具或者用过包管理器的电脑,几乎必踩。

怎么确认自己是不是遇到了这个问题?在终端里执行:

where python where pip

如果两行命令输出的路径不在同一个Python目录下,就说明pip和python根本不是一家人。这也是为什么很多资深开发者不直接推荐用pip install,而是建议用:

python -m pip install requests

4.2 python -m pip 和 pip 到底差在哪

pip单独执行时,实际上由操作系统去搜索名为pip的可执行文件,这个文件可能跟在哪个解释器后面都不确定;而python -m pip的意思是“启动当前python解释器,然后运行它自带的pip模块”。两者最终都能调起pip,但后者保证了你安装包所用的解释器,和后面运行代码时python所对应的解释器一定是同一个。

从那以后,我给自己立了一个规矩:只要和包安装有关的命令,一律写成python -m pip ...,不直接敲pip ...。这个习惯看着很小,却帮我省掉了后面几十次环境问题的排查时间。

如果你用Anaconda或者Miniconda管理环境,那其实还多了一个conda install的选择。但对第3次作业这种轻量级的任务,最稳妥的做法是给作业单独建一个虚拟环境,把依赖限定在里面,互不污染。

4.3 用venv把这次作业的环境锁死

Python官方自带的venv模块就是干这个的。它可以在项目目录里创建一个独立的Python运行环境,你在里面安装的所有第三方包都不会影响全局解释器。创建环境只需要一条命令:

python -m venv .venv

创建成功后,激活它。Windows系统是:

.venv\Scripts\activate

macOS或Linux是:

source .venv/bin/activate

激活后,终端提示符前面会出现(.venv)字样,这时候再安装依赖:

python -m pip install requests

后面运行脚本时,只要终端里还处于激活状态,用的就是环境里的解释器。这样requests装在哪、脚本在哪运行,永远都是同一个地方。整个过程可以整理成一张速查表:

操作Windows命令macOS/Linux命令
创建环境python -m venv .venvpython -m venv .venv
激活环境.venv\Scripts\activatesource .venv/bin/activate
安装依赖python -m pip install requestspython -m pip install requests
退出环境deactivatedeactivate

如果你用的是VS Code,还有个偷懒技巧:打开项目文件夹后,VS Code会自动识别.venv目录;你按Ctrl+Shift+P搜索“Python: Select Interpreter”,选带.venv的那个路径,哪怕忘了在终端里激活,编辑器也会优先用环境里的解释器跑代码。

提示:不要把这个虚拟环境目录提交给老师或者传到Git仓库里,它只是你本机的一个运行环境,别人打开项目时根据自己的系统重建一份即可。如果以后项目需要用代码托管,记得在.gitignore里加上.venv/

等到环境问题解决、requests能正常导入的那一瞬间,我第一次有了一种“像专业开发者那样管理项目”的感觉。环境问题看似跟作业的算法无关,但如果你不会管理它,你的代码写得再漂亮也跑不起来。

5. 交作业前,我调整了三个学习习惯

题目做完之后,理论上就可以提交了。但我把作业代码反复看了几遍,发现自己还有三个习惯不太对劲。调整之后,代码的“专业感”一下子提升了不少,这里也分享给你。

5.1 先看完整Traceback,别只读最后一行

之前我遇到报错,总是习惯去看终端输出最下面那行,比如ValueError: invalid literal for int()。其实Python报错最有价值的信息恰恰隐藏在整个Traceback里。举个例子:

Traceback (most recent call last): File "homework3.py", line 22, in <module> clean_list = clean_data(raw_list) File "homework3.py", line 15, in clean_data return [int(item) for item in items] ValueError: invalid literal for int() with base 10: 'abc'

最后一行告诉你错误类型是ValueError,但真正需要修改的是第15行那个列表推导式,里面的item居然是个字符串'abc'。如果你只盯着最后一行,可能会跑到第22行去检查,找半天也找不到问题。

我的新读法是:先看最后一行确认错误类型,然后往上翻,找到最靠近“你写的代码”的那一行,再结合出错的数据一起判断。“报错信息不是判你死刑的判决书,而是给你指路的箭头”,当你把报错当成线索而不是麻烦时,调试就变成了解谜过程。

5.2 文件路径写成相对路径前,先确认“当前目录”

第一次运行读取接口并保存文件时,我以为代码里写了文件名todo_1.json,它就会出现在代码文件旁边。结果运行完,文件根本不在那里,而是出现在了VS Code打开的工作区根目录。原因是open()里面写的相对路径是相对于“当前工作目录”的,而不是相对于代码文件所在目录的。如果你在终端里手动运行脚本时,工作目录是系统默认的C:\Users\用户名,那文件就会跑到那个地方去。

解决这个问题有两种思路。第一种是在终端里先cd到作业目录再运行脚本;第二种更稳妥,是用代码主动定位“当前代码文件在哪”。Python里推荐用pathlib

from pathlib import Path # 获取当前代码文件所在的目录 base_dir = Path(__file__).resolve().parent output_path = base_dir / "todo_1.json" with open(output_path, "w", encoding="utf-8") as file: file.write("...")

这样无论你从哪里运行这份代码,文件都会落在预期位置。第三次作业可能还没要求你掌握这么细的路径知识,但如果你以后处理数据文件、模型文件,路径问题绝对会反复出现。早一天开始用pathlib,就少一天被“找不到文件”折磨。

5.3 把每个小目标封装成函数,把“能跑”变成“可复用”

最后一个习惯是在提交前强制自己重构代码,把每个小目标封装成函数。很多人写作业的逻辑就是从上到下平铺,循环一个接一个,变量一个接一个,最后代码能跑就行。但是第3次作业开始有多个小目标了,把它们拆到函数里之后,代码会清晰很多。

我在这里分享一个朴素但非常好用的判断标准:如果你想把某段代码复制到另一个文件里用,说明它应该被抽成函数。比如“从接口获取数据”这段逻辑,我原本直接写在主流程里,后来发现要循环请求3个不同ID的数据,如果每次都复制一遍请求代码,脚本会变得又长又难维护。抽成fetch_todo(todo_id)函数之后,主流程只需要循环调用它就行。

函数命名也不要随便起,像get_data这种名字就太含糊了。改成fetch_todoparse_task_titlesave_to_file,读代码的人一看函数名就知道它干什么。Python本身不强制你这样做,但如果你后面打算把代码放到代码托管平台,或者和小伙伴组队写项目,这种“可读性投入”是绝对不会亏的。

至于是否要在这个阶段就引入类、装饰器那些更“高级”的概念?我的看法是,第3次作业还没到那个程度,硬要把一个简单脚本改造成面向对象反而显得别扭。先掌握“函数分解”这个最小单元,等到代码状态多到函数传参变复杂时,再学面向对象才是水到渠成的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询