Python第3次作业复盘:列表推导式、类型转换与环境排错

Python第3次作业复盘:列表推导式、类型转换与环境排错 说句实话刚看到“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 pythonpython命令能看到版本说明解释器装好了pip能输出版本说明包管理工具可用where能打印路径说明没有“多条Python混在一起”的隐患。这三步执行完再打开代码编辑器我用的是VS Code去跑作业环境问题基本能挡掉80%。提示如果用的是VS Code打开作业文件夹后按CtrlShiftP输入“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()做的事是把字符串解析成整数它不认识小数点。想处理带小数点的文本必须先转floatnumber 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, headersheaders, timeout10) print(状态码:, response.status_code) if response.status_code 200: data response.json() print(任务标题:, data[title]) with open(todo_result.json, w, encodingutf-8) as file: json.dump(data, file, ensure_asciiFalse, indent2)这段代码的流程是用requests.get请求接口检查返回状态码是不是200如果是就调用response.json()把服务器返回的JSON文本解析成Python字典然后打印标题最后用json.dump保存到文件。如果作业做到这里就能正常运行那你已经跑通了一条“网络请求—解析—落盘”的完整链路。3.2 第一次运行卡在编码和响应头这两个地方但这版代码不是一遍就跑通的我前后改了三个问题。第一个问题是运行之后没有任何输出因为接口返回了非200状态码。检查后才发现我不小心把URL拼错了一个字母。这个教训特别基础但也特别值得记住程序没有反应时先把URL复制到浏览器里访问一下看看它到底能不能通这能排除掉大量“看起来是代码问题实际是网络/地址问题”的情况。第二个问题是编码。我把完整响应打印出来时发现中文变成了乱码。原因是很多接口在响应头里没有明确声明charsetrequests库这时会按自己默认的编码去解码结果自然就错了。解决办法很直接拿到响应之后手动指定编码response.encoding utf-8第三个问题是保存文件时Windows的默认编码不是UTF-8所以我在open()里明确写了encodingutf-8否则后面在别的系统上打开文件就容易出现“UnicodeDecodeError”。再配合json.dump的ensure_asciiFalse保存下来的中文才是可读的而不是\uXXXX这样的转义序列。3.3 我在作业里最终交付的版本含逐行说明最后我把三个问题都改完交付版本里还额外做了一个小函数方便从不同ID的接口获取数据import requests import json from pathlib import Path def fetch_todo(todo_id: int) - dict: url fhttps://jsonplaceholder.typicode.com/todos/{todo_id} headers {User-Agent: python-homework/0.1 (learning python)} response requests.get(url, headersheaders, timeout10) 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_okTrue) output_file output_dir / ftodo_{current_id}.json with open(output_file, w, encodingutf-8) as file: json.dump(data, file, ensure_asciiFalse, indent2) 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 requests4.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\activatemacOS或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目录你按CtrlShiftP搜索“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里推荐用pathlibfrom pathlib import Path # 获取当前代码文件所在的目录 base_dir Path(__file__).resolve().parent output_path base_dir / todo_1.json with open(output_path, w, encodingutf-8) as file: file.write(...)这样无论你从哪里运行这份代码文件都会落在预期位置。第三次作业可能还没要求你掌握这么细的路径知识但如果你以后处理数据文件、模型文件路径问题绝对会反复出现。早一天开始用pathlib就少一天被“找不到文件”折磨。5.3 把每个小目标封装成函数把“能跑”变成“可复用”最后一个习惯是在提交前强制自己重构代码把每个小目标封装成函数。很多人写作业的逻辑就是从上到下平铺循环一个接一个变量一个接一个最后代码能跑就行。但是第3次作业开始有多个小目标了把它们拆到函数里之后代码会清晰很多。我在这里分享一个朴素但非常好用的判断标准如果你想把某段代码复制到另一个文件里用说明它应该被抽成函数。比如“从接口获取数据”这段逻辑我原本直接写在主流程里后来发现要循环请求3个不同ID的数据如果每次都复制一遍请求代码脚本会变得又长又难维护。抽成fetch_todo(todo_id)函数之后主流程只需要循环调用它就行。函数命名也不要随便起像get_data这种名字就太含糊了。改成fetch_todo、parse_task_title、save_to_file读代码的人一看函数名就知道它干什么。Python本身不强制你这样做但如果你后面打算把代码放到代码托管平台或者和小伙伴组队写项目这种“可读性投入”是绝对不会亏的。至于是否要在这个阶段就引入类、装饰器那些更“高级”的概念我的看法是第3次作业还没到那个程度硬要把一个简单脚本改造成面向对象反而显得别扭。先掌握“函数分解”这个最小单元等到代码状态多到函数传参变复杂时再学面向对象才是水到渠成的事。