AI 技术博客
返回首页
Python 基础 24 分钟阅读

阶段测验二:字符串、容器与文件(第 4-7 章)

Python基础阶段测验正则文件

引言

阶段测验一确认了前三章的基础语句与结构,本篇是阶段测验二,检验第 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 原对象」问题,两者其实是一枚硬币的两面。编程题分别实践了正则解析管线与字典分组聚合,前者是后续爬虫、日志系统的模板,后者是数据处理的最小原型。

练习与自检清单

  1. 修复练习:把第 2 题的 basket=[] 改成 basket=None 版本并重跑,观察两次输出变化;再想想第 1 题如果用 lambda i=i: i 修好闭包,输出会变成什么(答案导向:[0, 1, 2])。
  2. 增量练习:给日志统计加一个需求——统计 404 请求访问最多的路径(提示:先筛选 status == '404' 再对 request 里的路径计数,答案里如果用到 most_common(1) 要说明并列时返回哪一条)。
  3. 反向练习:把第 10 题改成「不 try,改用 line.strip().isdigit() 先行校验」的 LBYL 版本,运行结果应与原版一致;然后比较两种风格在「脏行类型更多(空行、小数、负数)」时的维护成本,说出你倾向哪种及原因(答案导向:EAFP 更省心,因为 isdigit() 覆盖不了小数与负数形态)。
  4. 自查清单:能默写闭包延迟绑定的成因与两种修法;能说出默认参数的求值时机;能区分浅拷贝/深拷贝/* 重复对嵌套影响的异同;能不看文档写出一个带命名分组的日志行正则;能解释 json.loads 为什么能把 \u674e 还原成汉字。全部打勾后,进入阶段测验三(第 8-9 章面向对象与命令行)。