简介:这份题库答案解析面向 Python 初学者与备考者,以 30 道填空题形式梳理了常用语法知识点,涵盖数学函数、数据类型、列表与元组、字符串、变量类型、运算符、列表推导式、字典、切片操作、转义字符、print 函数、lambda 表达式及排序算法等,适合用来检验基础掌握程度。资源压缩包内仅包含一个 PDF 文档,大小约 490KB,无需解压即可直接查看;目前已有 111 人浏览学习。文档逐题给出答案和简明解析,对列表切片、字典更新、内置函数与易错表达式进行了重点说明,便于自学时对照检查,也适合教师抽取题目做课堂小测或课后作业。整个题目按知识点排列,难度适中,整体编排清晰;既能帮助入门者快速定位薄弱环节,也能作为考前刷题、日常复习与备课参考的便携资料。
1. 练习题不是用来“做对”的,是用来“测错”的
刚接触 python 入门时,很多人会把“python练习题-答案-python习题答案.pdf”当成一份题库打包:前半本是题,后半本是答案,做题、对答案、改错,循环往复。但这个流程到第三周就会卡住:答案印刷在 PDF 里,错的题你得人工比对;做对的题,你也不知道是自己真的懂了,还是当时记住了答案。对于有 5 年 python 经验的人来说也一样,只是卡住的位置不同——你开始给同事出 Python 面试题,才发现手写答案和可执行答案之间隔着一条巨大的鸿沟。这篇博文就用一个从 PDF 习题集起步、到自动化判题与学习路径反馈的完整方案,讲清楚怎么把纸面答案变成可执行断言,让一份普通的习题答案集,变成一套真正运行得起来、数据能往回反馈的题库系统。
2. 把纸面答案变成可执行断言:单元测试是练习题的终点
2.1 正确打印的答案,不等于可判定的答案
一份习题答案 PDF 里的“答案”,本质上是一段人类可读的结果描述。比如“用 max 求出列表最大值,结果是 9”,再比如“对列表排序后输出 [1, 2, 3]”。人能看懂,但程序没法直接判定。更麻烦的是,很多答案描述省略了边界条件:整数溢出、空列表、浮点精度、顺序不确定的字典遍历,这些在 PDF 里往往只有一句话,而执行时每一个都能让结果翻天覆地。
常见做法是不要急着去读答案,先为每一道练习题建立“输入-行为-输出”三元组。输入是函数参数,行为是调用过程,输出是对照结果。把这个三元组写进一个断言语义里,判题就从“人眼比对文本”变成了“程序判定布尔值”。这一步是整篇方案的枢纽,也决定了后面自动批改能走多远。
2.2 用 unittest 提供的 assert 语义替代肉眼比对
Python 标准库 unittest 自带一套断言方法,拿来当判题引擎正好。不需要额外安装,也不需要引入 pytest,核心原因有两个:第一,unittest 的断言失败会抛出 AssertionError,天然适合“一题一判定”的结构;第二,它的输出格式、计数方式、失败信息都已经有人维护过,比自写 if else 更抗折腾。
import unittest class CheckExercise(unittest.TestCase): def test_max_of_list(self): # 输入:一个普通整数列表 # 预期输出:最大值 9 self.assertEqual(max([3, 5, 9]), 9) def test_sort_does_not_return_none(self): # 常见误用:直接用 list.sort() 作为返回值 self.assertIsNone([3, 1, 2].sort())逻辑说明:assertEqual判定值和类型是否相等,assertIsNone专门用来抓“排序函数返回 None”这一类经典错误。给练习题写断言时,建议把题目里容易误解的边界单独拆成一个测试方法,而不是在同一个方法里堆多个 assert——失败的报错会明确指到具体用例,而不是让你面对一大段输出猜错在哪。
2.3 浮点答案必须用近似断言:delta 参数的两个坑
计算圆周率、数值积分和机器学习模型输出的习题,答案永远不能用assertEqual直接比。常见做法是使用assertAlmostEqual,并且理解它的两个参数:places表示小数点后保留几位,delta表示绝对值误差边界。
class TestFloatExercise(unittest.TestCase): def test_pi_approx(self): # delta 表示允许误差,适合量级不确定的数值 self.assertAlmostEqual(22 / 7, 3.1415926, delta=0.01) def test_money_rounding(self): # places 表示小数点后精确位数,适合金额类保留两位 self.assertAlmostEqual(round(1.005, 2), 1.01, places=2)参数说明:delta判断的是abs(first - second) <= delta,它不受小数点位数影响,适合结果范围很大的题目;places判断的是四舍五入到指定位数后是否相等,适合有明确精度要求的题目。一个常用建议是:金融计算用places,科学计算用delta。如果你发现测试明明应该过却失败,先看是不是把delta=0.01写成了places=0.01——后者不会报错,但断言行为和你预期的完全不一样。
3. 用最小判题脚本,让 PDF 里的练习题自动跑分
3.1 从 PDF 提取习题文本:pypdf 加正则切题
要把“python练习题-答案-python习题答案.pdf”变成可判题的数据,第一步是提取文本。常见做法是用 pypdf 库,它轻量、纯 Python、不依赖系统库。PDF 提取出来的文本往往带有页眉页脚、断行和空白字符,所以第二步要按行清洗,再用正则把题目切块。
import re import pypdf def extract_exercises(path: str) -> list[str]: reader = pypdf.PdfReader(path) # 每页提取文本,空页返回空字符串,避免拼接报错 text = "\n".join(page.extract_text() or "" for page in reader.pages) # 清洗页眉页脚:删除包含页码和“第 X 页”的行 lines = [line.strip() for line in text.splitlines() if line.strip()] lines = [line for line in lines if not re.match(r"第\s*\d+\s*页", line)] joined = "\n".join(lines) # 按“数字+点+空格”或“数字+顿号”切分题目块 blocks = re.split(r"\n\s*(\d{1,2})[\.、]\s*", joined) exercises = [] for i in range(1, len(blocks), 2): title = blocks[i] body = blocks[i + 1] if i + 1 < len(blocks) else "" exercises.append(f"{title}. {body}") return exercises逻辑说明:page.extract_text() or ""是防止某页没有文字时报NoneType错误;正则\n\s*(\d{1,2})[\.、]匹配的是“换行 + 1 到 2 位数字 + 点或顿号”的题号格式。这里有个关键细节:分隔符前后都要保留,否则题号会丢失。返回值是完整题目文本,下一步再人工或半自动地把每道题转换为{"id": 1, "func": "max_of_list", "input": [3, 5, 9], "expected": 9}这样的判定记录。
3.2 判题器核心代码:超时保护与异常隔离
练习题代码里最常见的崩溃方式不是答案错,而是死循环和异常。整个判题脚本里必须要有的三样东西:超时控制、异常捕获、逐题隔离。我一般用concurrent.futures.ThreadPoolExecutor做超时,而不是signal——后者在 Windows 上支持不完整,换台机器就跑不起来了。
import json import time import traceback from concurrent.futures import ThreadPoolExecutor, TimeoutError def execute_with_timeout(func, args, timeout: int = 2): with ThreadPoolExecutor(max_workers=1) as pool: future = pool.submit(func, *args) try: return future.result(timeout=timeout) except TimeoutError: # 超时后取消任务,防止资源占用 future.cancel() return {"status": "timeout", "detail": f"超过 {timeout} 秒"} def run_tests(exercise: dict) -> dict: user_func = exercise.get("user_func") if user_func is None: return {"status": "noanswer", "detail": "未提交函数"} try: result = execute_with_timeout(user_func, exercise["input"]) if isinstance(result, dict) and "status" in result: return result expect = exercise["expected"] if abs(result - expect) <= exercise.get("delta", 1e-6): return {"status": "pass", "detail": f"got {result}"} return {"status": "fail", "detail": f"期望 {expect},实际 {result}"} except Exception as exc: return {"status": "error", "detail": "".join( traceback.format_exception_only(type(exc), exc))}逻辑说明:run_tests每次只执行一个函数,失败时返回结构化的字典而不是直接抛异常,这样外层循环可以继续判下一题。delta从题目配置里读取,默认1e-6,解决浮点数比较问题。超时时间timeout=2是经验值,覆盖绝大多数练习题;遇到复杂度高的题,比如动态规划或大数计算,可以按题目单独调大。
3.3 判题参数速查表与失败怎么看
| 参数 | 类型 | 默认值 | 用途与建议 |
|---|---|---|---|
timeout | int | 2 | 单题执行上限,单位秒;死循环必卡在这里 |
delta | float | 1e-6 | 数值答案的绝对误差,题目要求精度高时调小 |
input | list | 必填 | 传给函数的参数列表,注意元组会被当成单参数 |
expected | 任意 | 必填 | 参考答案,必须是可比较的 Python 对象 |
status | str | 无 | pass/fail/error/timeout,脚本依据它统计 |
detail | str | 无 | 失败详情,写入报告时保留原始异常信息 |
这个表格里的status区分了“答案错”和“代码崩”,这是判题脚本和人类批改最大的差异。你拿到一份 Python 作业时,第一眼要看的是error和timeout两类,因为它们说明代码根本没跑完,而不是答案不正确。fail表示运行成功但结果与答案不一致,这两种现象对应的排查路径完全不同,混在一起会让后续数据统计失真。
3.4 把判题结果写成 JSON:给后续统计铺路
判题脚本的最后一步是把结果持久化。我一般会把每道题的输出追加到一个 JSONL 文件里,一行一道题,而不是用一个巨大的 JSON 文件。原因是:判题过程可能中断,JSONL 天然支持增量写入,恢复时只需要跳过已有的题目编号。
def save_result(result: dict, history_path: str): line = json.dumps(result, ensure_ascii=False) with open(history_path, "a", encoding="utf-8") as f: f.write(line + "\n")result = { "id": 2, "ts": time.time(), "status": "pass", "detail": "got 9", "topic": ["列表", "内置函数"], "cost_ms": 1.2 } save_result(result, "results.jsonl")逻辑说明:ensure_ascii=False保证中文知识点标签以原文写入文件,方便后续生成错题报告;ts记录时间戳,为后面画“记忆衰减曲线”准备好数据;topic是人工给题目打的标签,一份练习册大概要花半小时给所有题目标好知识点,但这一半小时花得很值——第 4 章的归因分析全靠它。
4. 错题归因:用正确率数据找出卡住学习路径的知识点
4.1 用 pandas 聚合成绩,画出知识点正确率热力图
判题脚本跑完之后,你手上有的是一个results.jsonl文件,里面每条记录带topic和status。这时候如果还在按“题号”刷题,就浪费了数据。我建议用 pandas 读入全部记录,按知识点标签聚合正确率,再画一张热力图。热力图的横轴可以是日期,纵轴是知识点,颜色深代表错误率高。
import pandas as pd import matplotlib.pyplot as plt records = [] with open("results.jsonl", encoding="utf-8") as f: for line in f: records.append(json.loads(line)) df = pd.DataFrame(records) df["date"] = pd.to_datetime(df["ts"], unit="s").dt.date df["ok"] = (df["status"] == "pass").astype(int) pivot = df.pivot_table(index="topic", columns="date", values="ok", aggfunc="mean") fig, ax = plt.subplots(figsize=(8, 6)) # 冷色表示正确率高,暖色表示错误率高 im = ax.imshow(pivot.values, cmap="RdYlGn", aspect="auto") ax.set_xticks(range(len(pivot.columns))) ax.set_xticklabels(pivot.columns, rotation=45) ax.set_yticks(range(len(pivot.index))) ax.set_yticklabels(pivot.index) fig.colorbar(im, label="正确率") plt.tight_layout() plt.savefig("topic_heatmap.png", dpi=150)代码说明:pivot_table的行是topic知识点,列是日期,值是当天该知识点的平均正确率。aggfunc="mean"表示数值越大越接近全对。这张图最直接的用途是找到“连续三天都在 ZC/0.5 附近徘徊的知识点”,而不是只看今天的错题。当前正确率低并不危险,危险的是同一个知识点反复出现低正确率,那说明底层的 python 基础语法概念没打通。
4.2 用知识点标签生成错题集 Markdown
热力图告诉你“哪些知识点危险”,但真正要回到题目上去刷,需要一份按知识点聚合的错题清单。用 pandas 过滤出所有status != "pass"的记录,按topic分组,再渲染成 Markdown 表格,可以直接导入笔记软件或打印出来。
wrong = df[df["status"] != "pass"].sort_values(["topic", "date"]) report_lines = ["# 错题集", ""] for topic, group in wrong.groupby("topic"): report_lines.append(f"## {topic}") report_lines.append("") report_lines.append("| 日期 | 题号 | 状态 | 详情 |") report_lines.append("| --- | --- | --- | --- |") for row in group.itertuples(): report_lines.append( f"| {row.date} | {row.id} | {row.status} | {row.detail} |" ) report_lines.append("") with open("wrong_report.md", "w", encoding="utf-8") as f: f.write("\n".join(report_lines))逻辑说明:sort_values(["topic", "date"])保证同一知识点的错题按时间排好,方便复查时看出错误形态有没有变化。detail字段里保存的是“期望 9,实际 8”,这是让你快速回忆当时错误的关键信息。生成的文件直接覆盖上一次结果,因为这份错题集的目的是“当前仍错的题”,而不是历史全量记录。该格式和印象笔记、Obsidian 都能直接兼容。
4.3 时间序列折线:判断知识点的巩固程度
正确率热力图是横截面视角,折线图则是时间序列视角。同一个知识点,如果连着学习三天正确率从 0.3 涨到 0.8,说明学习有效;如果第一天 0.8、第二天 0.9、第三天 0.4,基本可以判断是记忆衰减而非能力不足。画折线图不需要额外聚合,df里已经带了date和ok字段,直接按topic过滤画线。
for topic in df["topic"].unique(): subset = df[df["topic"] == topic].sort_values("date") subset_group = subset.groupby("date")["ok"].mean() plt.plot(subset_group.index, subset_group.values, marker="o", label=topic) plt.xlabel("日期") plt.ylabel("正确率") plt.legend() plt.savefig("topic_trend.png", dpi=150)这段图的价值在于帮你决定“今天要不要刷这个知识点”。正确率连续三天高于 0.8 的知识点,可以暂时移出每日练习队列;正确率从高位跌下来的知识点,说明需要重新过一遍基础语法和例题。判断题刷完的标准不是“全部做对”,而是“每个知识点的最近三次正确率都稳定在 0.8 以上”。
4.4 调整选题策略:从固定练习册切换到动态队列
有了历史数据,就不必再从头到尾刷那份 PDF。我会把题目当成一个池子,每个知识点保留最近 10 道题,动态组成当天的练习队列。选题规则很简单:优先选择正确率低于 0.6 的知识点,每个点抽 2 题;正确率高于 0.8 的知识点,每天只抽 1 题用于保持手感。这套逻辑可以写成一个十几行的函数,不需要引入调度框架,纯用df过滤就能实现。
5. 用“答案无关”断言保护练习题的价值
5.1 固定答案断言的最大缺陷:抄答案和背答案
到这一步,你手头已经有一套能自动判题、能统计错题的脚本。但还有一个隐藏问题没解决:如果答案是以“预期输出”形式固定写在配置里的,那么做题的人可以不走任何逻辑推理,直接print(9)也能通过断言。这一章要解决的就是这个。核心思路是把断言从“值相等”改成“性质满足”。
5.2 用不变量检查替代具体值断言
不变量是指不管输入怎么变化,输出必须满足的数学性质。以排序题为例,不变量有两个:输出长度和输入长度相等;输出列表是输入列表的一个排列。这两条不通过,结果一定错。用 hypothesis 库可以自动生成多组输入来验证,不需要手写一堆测试用例。
from hypothesis import given, strategies as st @given(st.lists(st.integers(), min_size=0, max_size=20)) def test_sort_invariants(arr): result = sorted(arr) assert len(result) == len(arr) assert sorted(result) == sorted(arr)逻辑说明:st.lists生成一组随机列表,@given装饰器让这个测试自动跑几百组。前面的断言判断长度一致,后面的断言判断元素集合一致。对于练习题的自动判题来说,不变量检查特别适合算法题,因为算法题的输入空间很大,参考答案只是其中一条路径,而性质检查才是对“理解程度”的验证。
5.3 用 AST 扫描拦截硬编码输出
背答案的高级形态是:判断输入是[3, 5, 9]时直接返回 9,其他情况假装报错。要防住这种写法,可以用 Python 自带的 ast 模块扫描提交代码的语法树,检查是否出现了针对特定输入的 if 分支或者直接以print硬编码答案。
import ast def detect_hardcode(source: str) -> bool: tree = ast.parse(source) for node in ast.walk(tree): if isinstance(node, ast.Constant) and node.value == 9: return True return False参数说明:这段代码只做了最基础的检查——语法树里是否出现常量9。更完整的方案可以升级为“统计常量出现的位置和频率”,或者检查函数体内是否缺少return语句。注意危险点:这个检测本身也有误报,一个合法的排序函数里完全可能包含9。所以 AST 扫描只适合作为“提示”,不作为自动判题的硬性通过条件,误杀率高的规则宁可不加。
5.4 把这套判题链挂进 CI,做一个回归保护
最后一步,我一般会把这套判题脚本放到任意支持 shell 的 CI 系统里,定时或在提交代
本文还有配套的精品资源,点击获取