引言
阶段测验一确认了前三章的基础语句与结构,本篇是阶段测验二,检验第 4-7 章——函数(def、作用域、闭包、lambda)、字符串与正则(方法、切片、编码、re 模块)、容器(列表、元组、字典、集合、拷贝)、文件与异常(读写、编码、pathlib、错误处理)。这四章的共同特征是「知识密度高、坑密集」,学完每章的章末测验只能证明单章过关,本测验把四章内容混在一起出题,检验的是「跨章节的诊断能力」:看到一段代码,能同时调用函数作用域、字符串、容器、文件四个知识库去推断输出。全部代码均在本机 CPython 3.12.10(Windows,Git Bash)实测,每题代码块都自包含、可以直接粘贴运行。
测验说明:覆盖范围与难度分布
十道选择题按「记忆 → 理解 → 应用」梯度排列,每题标注出处:
- 第 4 章(函数):3 题——闭包延迟绑定、可变默认参数、
count += 1的作用域陷阱。这三个是函数章节杀伤力最大的三个点,也是后续装饰器、OOP 章节反复出现的前置知识。 - 第 5 章(字符串与正则):3 题——字符串方法不修改原串、命名分组提取、
re.sub的count参数。正则题刻意避开单章测验考过的 findall 元组输出,改考命名分组与替换边界。 - 第 6 章(容器):2 题——
[[]] * 3的引用共享、字典copy()的浅拷贝语义。 - 第 7 章(文件与异常):2 题——JSON 反序列化里的 Unicode 转义、逐行读文件时的脏数据防御。
难度分布:记忆 1 题、理解 4 题、应用 5 题——四章内容里「应用」占比最高,因为文件与正则的组合本质就是应用。建议每题限时两分半,做完先不翻解析,直接进编程题;若编程题卡壳,再回来对照选择题的解析找知识缺口。
选择题(共 10 题,每题只有一个正确答案)
第 1 题:循环里的 lambda 闭包【理解】(出处:第 4 章「作用域与命名空间」「lambda 函数」)
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])
输出是什么?
A. [0, 1, 2]
B. [2, 2, 2]
C. [1, 2, 3]
D. 抛 NameError
第 2 题:可变默认参数【记忆】(出处:第 4 章「def 与 return」)
def add_item(item, basket=[]):
basket.append(item)
return basket
print(add_item('苹果'))
print(add_item('香蕉'))
输出是什么?
A. ['苹果'] 和 ['香蕉']
B. ['苹果'] 和 ['苹果', '香蕉']
C. ['苹果', '香蕉'] 和 ['苹果', '香蕉']
D. 抛 TypeError
第 3 题:闭包里的 count += 1【理解】(出处:第 4 章「作用域与命名空间」)
def make_counter():
count = 0
def increment():
count += 1
return count
return increment
counter = make_counter()
print(counter())
运行结果是什么?
A. 输出 1
B. 输出 0
C. 抛 UnboundLocalError
D. 输出 <function make_counter...>
第 4 题:字符串方法是否修改原串【记忆】(出处:第 5 章「字符串方法大全」)
s = 'Hello, Python'
print(s.upper())
print(s)
t = ' a,b,c '
print(t.strip().split(','))
输出是什么?
A. HELLO, PYTHON、HELLO, PYTHON、['a', 'b', 'c']
B. hello, python、Hello, Python、['a', 'b', 'c']
C. HELLO, PYTHON、Hello, Python、['a', 'b', 'c']
D. HELLO, PYTHON、Hello, Python、[' a', 'b', 'c ']
第 5 题:正则命名分组提取【应用】(出处:第 5 章「正则进阶:分组、捕获与非贪婪」)
import re
line = '192.168.1.1 - - [04/Sep/2026:10:00:00 +0800] "GET /index.html HTTP/1.1" 200 1024'
m = re.search(r'(?P<ip>[\d.]+) - - \[.*?\] ".*?" (?P<status>\d{3})', line)
print(m.group('ip'), m.group('status'))
输出是什么?
A. 192.168.1.1 200
B. 192.168.1.1 GET
C. 10:00:00 200
D. 抛 AttributeError(没有匹配)
第 6 题:re.sub 的 count 参数【应用】(出处:第 5 章「re 模块实战」)
import re
text = '订单 100 号,金额 200 元,数量 300 件'
print(re.sub(r'\d+', '#', text, count=2))
输出是什么?
A. 订单 # 号,金额 # 元,数量 # 件
B. 订单 # 号,金额 # 元,数量 300 件
C. 订单 # 号,金额 200 元,数量 300 件
D. 抛 ValueError:count 参数不存在
第 7 题:[[]] * 3 的引用共享【理解】(出处:第 6 章「浅拷贝与深拷贝」)
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
输出是什么?
A. [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
B. [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
C. [[1, 0, 0], [1, 0, 0], [0, 0, 0]]
D. 抛 TypeError:元组不能修改
第 8 题:字典的浅拷贝【理解】(出处:第 6 章「浅拷贝与深拷贝」)
d = {'items': [1, 2]}
d2 = d.copy()
d2['items'].append(3)
print(d)
print(d2 == d)
输出是什么?
A. {'items': [1, 2, 3]} 和 True
B. {'items': [1, 2]} 和 False
C. {'items': [1, 2]} 和 True
D. {'items': [1, 2, 3]} 和 False
第 9 题:JSON 反序列化里的 Unicode 转义【应用】(出处:第 7 章「CSV 与 JSON 文件」)
import json
data = json.loads('{"name": "\\u674e\\u56db", "score": 92}')
print(data['name'])
print(data.get('city', '未知'))
输出是什么?
A. \u674e\u56db 和 未知
B. 李四 和 未知
C. 李四 和 抛 KeyError
D. \u674e\u56db 和 抛 KeyError
第 10 题:逐行读文件时的脏数据【应用】(出处:第 7 章「文件异常与防御式读写」「异常处理」)
from pathlib import Path
Path('scores.txt').write_text('90\nabc\n70\n', encoding='utf-8')
total = 0
count = 0
for line in Path('scores.txt').open(encoding='utf-8'):
try:
total += int(line.strip())
count += 1
except ValueError:
print('跳过脏数据')
print(total / count)
输出是什么?
A. 跳过脏数据 和 80.0
B. 跳过脏数据 和 80
C. 只打印 跳过脏数据
D. 程序崩溃:int() invalid literal for ‘abc’
点击查看答案
答案:B B C C A B B A B A
第 1 题(B):三个 lambda 捕获的是同一个循环变量 i,循环结束时 i 停在 2,三个函数被调用时读到的都是最终值 2,所以输出 [2, 2, 2]。闭包延迟绑定(late binding)是作用域章节的核心坑:闭包记录的是环境本身,不是绑定发生那一刻的值。正确的延迟方案是默认参数 lambda i=i: i(绑定发生即定格)或生成器/函数工厂。第 4 章章末测验的 LEGB 题考的是读变量,这里考的是「何时读」,难度升一级。
第 2 题(B):有默认值的参数在 def 执行时只创建一次对象,basket=[] 这个列表被所有「没传第二参数」的调用共享。第一次调用把它变成 ['苹果'],第二次调用拿到的还是同一个列表,继续追加成 ['苹果', '香蕉']。要修正就改用 basket=None,函数体内 basket = [] if basket is None else basket。判断依据一句话:默认值属于定义时刻,不属于调用时刻。
第 3 题(C):count += 1 让 count 在 increment 内部被绑定,于是 PEP 8 式的作用域规则判定 count 是 increment 的局部变量,可它又被读取于任何赋值之前,于是抛 UnboundLocalError。CPython 3.12 的报错文案很直白:cannot access local variable 'count' where it is not associated with a value。想让闭包修改外层变量,必须声明 nonlocal count——这正是「闭包雏形」篇里 nonlocal 存在的理由。注意它和第五章的 VariableError 无关,这是作用域规则,不是语法错误。
第 4 题(C):字符串不可变,所有「变换类」方法(upper、strip、replace、split 等)都返回新串而不是修改原串:s.upper() 打印 HELLO, PYTHON,随后打印 s 仍是 Hello, Python。t.strip() 去掉首尾空白得到 'a,b,c',再 .split(',') 得到 ['a', 'b', 'c']。逐行 strip().split() 的连写是解析文本文件的标准起手式。
第 5 题(A):(?P<ip>...) 是命名分组,search 从任意位置找第一个匹配,m.group('ip') 与 m.group('status') 按名字取值。模式里的 .*? 是非贪婪匹配状态码前面的请求串,(?P<ip>[\d.]+) 匹配点分十进制 IP。日志行的「提取字段」正是为第 5 章第 9 篇的 nginx 日志解析器打的基础。
第 6 题(B):re.sub(pattern, repl, string, count=2) 最多替换前 2 处匹配:100 和 200 被换成 #,300 保持原样,输出 订单 # 号,金额 # 元,数量 300 件。subn 则额外返回替换次数。四个参数的位置顺序是 pattern、repl、string、count,前三个几乎每次都要写,count 靠关键字传最稳妥。
第 7 题(B):[0] * 3 得到 [0, 0, 0],这是值复制,没问题;但外层 [[0] * 3] * 3 是把同一个内层列表引用重复了 3 次,三个「行」其实指向同一个对象。grid[0][0] = 1 改的是那个共享对象,于是三行都被改写。验证办法:print(grid[0] is grid[1]) 会打印 True。创建真正的二维列表要用列表推导式 [[0] * 3 for _ in range(3)]——每次迭代都新建一个内层列表。
第 8 题(A):d.copy() 是浅拷贝(shallow copy):复制了顶层字典,但值是引用、不复制。d2['items'] 与 d['items'] 是同一个列表,append(3) 同时改了两个字典可见的内容,所以 d 变成 {'items': [1, 2, 3]},且 d2 == d 为 True(内容相同)。要彻底隔离就用 copy.deepcopy。判断深浅拷贝的通用问题就一句:「修改拷贝里的嵌套对象,原对象变不变」。这题和第 7 题是同一原理的两种表现。
第 9 题(B):json.loads 按 JSON 语法解析 \u674e 为码点转义,还原成汉字「李」,「\u56db」还原成「四」,所以 data['name'] 是 李四。它与 Python 字符串里的 \u 转义是同一套 Unicode 转义体系,只是 JSON 用自己的解析器处理。data.get('city', '未知') 演示了字典取值的防御姿势:键不存在时返回默认值而不是抛 KeyError——这正是第 6 章字典篇 get 方法的工程价值。
第 10 题(A):逐行迭代文件对象,'abc' 转 int 抛 ValueError 被 except ValueError 接住,打印「跳过脏数据」并跳过该行;90 与 70 正常累加,count 为 2,80.0 是浮点除法的结果。这就是 EAFP(先斩后奏,better to ask forgiveness than permission)风格:先尝试再捕获,用异常处理不可信数据,而不是先写一堆检查。若去掉 try 块,程序会在 'abc' 处崩溃——脏数据防御是文件处理区别于「文件打开/关闭」的第二课。
编程题(共 2 题,附可运行参考解答)
编程题 1:日志统计
题目:写一个 log_stats.py:内置一段 nginx combined 格式的样例日志(写入 access.log 后读取),用正则+命名分组解析每一行,统计:①各状态码出现次数(按次数降序);②访问量 TOP3 的 IP。要求解析逻辑独立成 parse_line(line) 与 summarize(lines) 两个纯函数,Counter 用于计数。
参考解答:
"""日志统计:解析 nginx combined 格式日志,输出状态码分布与访问 TOP3 IP。"""
import re
from collections import Counter
from pathlib import Path
LOG_LINE = re.compile(
r'(?P<ip>[\d.]+) - - \[(?P<time>[^\]]+)\] '
r'"(?P<request>[^"]+)" (?P<status>\d{3}) (?P<size>\d+)'
)
def parse_line(line):
m = LOG_LINE.search(line)
return m.groupdict() if m else None
def summarize(lines):
status_counter = Counter()
ip_counter = Counter()
for line in lines:
row = parse_line(line)
if row is None:
continue
status_counter[row['status']] += 1
ip_counter[row['ip']] += 1
return status_counter, ip_counter
def main():
sample = """\
192.168.1.10 - - [04/Sep/2026:10:01:02 +0800] "GET /index.html HTTP/1.1" 200 5120
192.168.1.20 - - [04/Sep/2026:10:01:05 +0800] "GET /images/logo.png HTTP/1.1" 404 1024
192.168.1.30 - - [04/Sep/2026:10:02:33 +0800] "POST /api/login HTTP/1.1" 200 128
192.168.1.10 - - [04/Sep/2026:10:02:10 +0800] "GET /docs HTTP/1.1" 500 655
192.168.1.10 - - [04/Sep/2026:10:03:01 +0800] "GET /index.html HTTP/1.1" 200 5120
192.168.1.20 - - [04/Sep/2026:10:04:44 +0800] "GET /index.html HTTP/1.1" 200 5120
"""
Path('access.log').write_text(sample, encoding='utf-8')
status_counter, ip_counter = summarize(
Path('access.log').read_text(encoding='utf-8').splitlines()
)
print('状态码分布:')
for status, count in status_counter.most_common():
print(f' {status}: {count} 次')
print('访问 TOP3 IP:')
for ip, count in ip_counter.most_common(3):
print(f' {ip}: {count} 次')
if __name__ == '__main__':
main()
本机实测运行 python log_stats.py 输出:
状态码分布:
200: 4 次
404: 1 次
500: 1 次
访问 TOP3 IP:
192.168.1.10: 3 次
192.168.1.20: 2 次
192.168.1.30: 1 次
关键点:parse_line 对无法匹配的行返回 None,summarize 直接跳过——脏行不炸程序,这是第 7 章 EAFP 思想的又一应用;Counter.most_common() 在计数相同(404 与 500 各 1 次)时按「首次出现顺序」排列,所以 404 排在 500 前面,这个细节来自 collections.Counter 的实现(第一次遇到元素的顺序),写文档或断言时要知道。正则一次编译、多处使用,是第 5 章推荐的预编译姿势。
编程题 2:成绩汇总
题目:写一个 score_summary.py:scores.txt 里每行是「班级,姓名,成绩」三条逗号分隔记录(两班共六条),程序读取后:①按班级分组;②每班按成绩降序排序并输出平均分(一位小数)与前三名(姓名(成绩) 格式);③把各班平均分写成 summary.txt。要求分组用 setdefault 或 defaultdict 完成,排序用 sorted(..., key=..., reverse=True)。
参考解答:
"""成绩汇总:按班级统计平均分、前三名,并把汇总写回 summary.txt。"""
from pathlib import Path
SCORE_LINES = """\
一班,李雷,88
二班,韩梅梅,92
一班,王芳,79
二班,李华,85
一班,赵磊,93
二班,孙悦,90
"""
def load_scores(text):
"""把 '班级,姓名,成绩' 文本解析为 {班级: [(姓名, 成绩), ...]}。"""
groups = {}
for line in text.splitlines():
if not line.strip():
continue
class_name, name, score = line.split(',')
groups.setdefault(class_name, []).append((name, int(score)))
return groups
def average(scores):
return sum(scores) / len(scores)
def main():
Path('scores.txt').write_text(SCORE_LINES, encoding='utf-8')
groups = load_scores(Path('scores.txt').read_text(encoding='utf-8'))
summary_lines = []
for class_name, rows in groups.items():
rows.sort(key=lambda row: row[1], reverse=True)
avg = average([score for _, score in rows])
top3 = '、'.join(f'{name}({score})' for name, score in rows[:3])
print(f'{class_name}:平均 {avg:.1f} 分,前三名:{top3}')
summary_lines.append(f'{class_name} 平均分 {avg:.1f}')
Path('summary.txt').write_text('\n'.join(summary_lines) + '\n', encoding='utf-8')
print('summary.txt 内容:')
print(Path('summary.txt').read_text(encoding='utf-8').rstrip())
if __name__ == '__main__':
main()
本机实测运行 python score_summary.py 输出:
一班:平均 86.7 分,前三名:赵磊(93)、李雷(88)、王芳(79)
二班:平均 89.0 分,前三名:韩梅梅(92)、孙悦(90)、李华(85)
summary.txt 内容:
一班 平均分 86.7
二班 平均分 89.0
关键点:setdefault(class_name, []).append(...) 一行完成「没有键就建空列表再追加」的经典分组模式;sorted 的 key=lambda row: row[1] 按成绩降序排序,取前三条即前三名;{:.1f} 负责一位小数。这道题把第 4 章的函数、第 5 章的字符串、第 6 章的字典与排序、第 7 章的文件读写全部串在一次运行里,是做错的同学最该动手重写一遍的题目——先自己实现,再对照参考解查找差距。
小结
本测验覆盖 4-7 章的高频坑:闭包延迟绑定与 nonlocal、一次创建的可变默认参数、只产生新对象的字符串方法、命名分组与 sub 的 count、* 重复与浅拷贝的引用共享、json.loads 的 Unicode 转义、逐行读文件的 EAFP 脏数据防御。这些坑的共同根源只有一个——理解「引用、拷贝与所有权」:函数默认值、列表重复、字典 copy 是引用问题,字符串方法与 JSON 是「新对象 vs 原对象」问题,两者其实是一枚硬币的两面。编程题分别实践了正则解析管线与字典分组聚合,前者是后续爬虫、日志系统的模板,后者是数据处理的最小原型。
练习与自检清单
- 修复练习:把第 2 题的
basket=[]改成basket=None版本并重跑,观察两次输出变化;再想想第 1 题如果用lambda i=i: i修好闭包,输出会变成什么(答案导向:[0, 1, 2])。 - 增量练习:给日志统计加一个需求——统计 404 请求访问最多的路径(提示:先筛选
status == '404'再对 request 里的路径计数,答案里如果用到most_common(1)要说明并列时返回哪一条)。 - 反向练习:把第 10 题改成「不 try,改用
line.strip().isdigit()先行校验」的 LBYL 版本,运行结果应与原版一致;然后比较两种风格在「脏行类型更多(空行、小数、负数)」时的维护成本,说出你倾向哪种及原因(答案导向:EAFP 更省心,因为isdigit()覆盖不了小数与负数形态)。 - 自查清单:能默写闭包延迟绑定的成因与两种修法;能说出默认参数的求值时机;能区分浅拷贝/深拷贝/
*重复对嵌套影响的异同;能不看文档写出一个带命名分组的日志行正则;能解释json.loads为什么能把\u674e还原成汉字。全部打勾后,进入阶段测验三(第 8-9 章面向对象与命令行)。