拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python技术:把PDF练习题变成自动判题系统的完整方案

简介这份题库答案解析面向 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, delta0.01) def test_money_rounding(self): # places 表示小数点后精确位数适合金额类保留两位 self.assertAlmostEqual(round(1.005, 2), 1.01, places2)参数说明delta判断的是abs(first - second) delta它不受小数点位数影响适合结果范围很大的题目places判断的是四舍五入到指定位数后是否相等适合有明确精度要求的题目。一个常用建议是金融计算用places科学计算用delta。如果你发现测试明明应该过却失败先看是不是把delta0.01写成了places0.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_workers1) as pool: future pool.submit(func, *args) try: return future.result(timeouttimeout) 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: fgot {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解决浮点数比较问题。超时时间timeout2是经验值覆盖绝大多数练习题遇到复杂度高的题比如动态规划或大数计算可以按题目单独调大。3.3 判题参数速查表与失败怎么看参数类型默认值用途与建议timeoutint2单题执行上限单位秒死循环必卡在这里deltafloat1e-6数值答案的绝对误差题目要求精度高时调小inputlist必填传给函数的参数列表注意元组会被当成单参数expected任意必填参考答案必须是可比较的 Python 对象statusstr无pass/fail/error/timeout脚本依据它统计detailstr无失败详情写入报告时保留原始异常信息这个表格里的status区分了“答案错”和“代码崩”这是判题脚本和人类批改最大的差异。你拿到一份 Python 作业时第一眼要看的是error和timeout两类因为它们说明代码根本没跑完而不是答案不正确。fail表示运行成功但结果与答案不一致这两种现象对应的排查路径完全不同混在一起会让后续数据统计失真。3.4 把判题结果写成 JSON给后续统计铺路判题脚本的最后一步是把结果持久化。我一般会把每道题的输出追加到一个 JSONL 文件里一行一道题而不是用一个巨大的 JSON 文件。原因是判题过程可能中断JSONL 天然支持增量写入恢复时只需要跳过已有的题目编号。def save_result(result: dict, history_path: str): line json.dumps(result, ensure_asciiFalse) with open(history_path, a, encodingutf-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_asciiFalse保证中文知识点标签以原文写入文件方便后续生成错题报告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, encodingutf-8) as f: for line in f: records.append(json.loads(line)) df pd.DataFrame(records) df[date] pd.to_datetime(df[ts], units).dt.date df[ok] (df[status] pass).astype(int) pivot df.pivot_table(indextopic, columnsdate, valuesok, aggfuncmean) fig, ax plt.subplots(figsize(8, 6)) # 冷色表示正确率高暖色表示错误率高 im ax.imshow(pivot.values, cmapRdYlGn, aspectauto) ax.set_xticks(range(len(pivot.columns))) ax.set_xticklabels(pivot.columns, rotation45) ax.set_yticks(range(len(pivot.index))) ax.set_yticklabels(pivot.index) fig.colorbar(im, label正确率) plt.tight_layout() plt.savefig(topic_heatmap.png, dpi150)代码说明pivot_table的行是topic知识点列是日期值是当天该知识点的平均正确率。aggfuncmean表示数值越大越接近全对。这张图最直接的用途是找到“连续三天都在 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, encodingutf-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, markero, labeltopic) plt.xlabel(日期) plt.ylabel(正确率) plt.legend() plt.savefig(topic_trend.png, dpi150)这段图的价值在于帮你决定“今天要不要刷这个知识点”。正确率连续三天高于 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_size0, max_size20)) 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 系统里定时或在提交代本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门