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

收官综合测验:全书知识大盘点

Python综合测验收官复习自检清单

引言

这是第十章(阶段测验与查漏补缺)的第十篇,也是课程 01 阶段一(第 1-10 章)的收官篇。前九篇依次完成了三场阶段测验、常见坑总结、代码风格审查、性能初体验、调试训练、通讯录里程碑项目和学习路线图,本篇把这些内容全部串成一场”总检验”:12 道选择题横跨全部十章(每道标注出处章),4 道编程题要求把多章技能组合使用,最后附一份 20 项的学习自检总清单。

范围声明:本测验的全部题目严格限定在课程 01 阶段一的知识范围内,不引入课程 02(数据结构与算法)及以后任何课程的内容——你不需要”提前偷学”才能做对,反过来,凡是做错的题都说明有章节需要回炉。运行环境声明:文中所有代码块与输出均基于本机 CPython 3.12.10(Windows,Git Bash)实测,输出与展示逐字一致;个别数值(如计时结果)随机器浮动,已在相应位置标注。

建议的使用方式:先独立完成 12 道选择题(不写代码也能凭推理作答,写代码验证更好),再对照解析;编程题先自己动手写,写不出来再折叠看参考解答——这一步”先想后看”是全篇唯一无法替代的练习。

命题原则与答题策略

选择题按「记忆→理解→应用」三档难度排列:记忆题考”是什么”(API 的行为与默认值),理解题考”为什么”(机制与原理),应用题考”会怎样”(给定代码推输出)。每道题标注它主要出自第几章,方便你答错后精准回炉:第 1 章环境与运行原理、第 2 章变量类型与运算符、第 3 章流程控制、第 4 章函数、第 5 章字符串与正则、第 6 章列表元组字典、第 7 章文件与异常、第 8 章面向对象、第 9 章命令行工具与日志测试、第 10 章本章(性能与调试)。作答时记住三条策略:先审题再代入(多数题的最优解是把代码在脑子里逐行”执行”一遍);警惕”看起来对”的选项(错项往往只改一个细节,比如深浅拷贝、门槛级别);拿不准就写下来跑一遍——本站一切结论都可以用第 1 章的 REPL 当场验证。

选择题(共 12 题,每题只有一个正确答案)

第 1 题:虚拟环境的隔离边界【记忆·第 1 章】

在项目根目录执行 python -m venv .venv 并激活该虚拟环境后,以下哪个说法正确?

A. 在该虚拟环境中执行 pip install 安装的包,只写入该虚拟环境自己的 site-packages,不影响系统里其他项目的环境。 B. 虚拟环境创建完成后,还需要手动把解释器路径加入系统 PATH,否则 python 命令不可用。 C. 每次创建虚拟环境都会重新下载并安装一份独立的 Python 安装包。 D. venv 是第三方工具包,需要先执行 pip install venv 才能使用。

第 2 题:f-string 的格式化精度【记忆·第 2 章】

pi = 3.14159
print(f'圆周率约为 {pi:.2f}')

输出是什么?

A. 圆周率约为 3.1416 B. 圆周率约为 3.140 C. 圆周率约为 3.14 D. 圆周率约为 3.14159

第 3 题:for 循环的 else 子句【理解·第 3 章】

for n in range(2, 6):
    for d in range(2, n):
        if n % d == 0:
            print(f'{n} 是合数')
            break
    else:
        print(f'{n} 是质数')

输出是什么?

A. 2 是质数、3 是质数、4 是质数、5 是质数 B. 2 是质数、3 是质数、4 是合数、5 是质数 C. 2 是合数、3 是质数、4 是合数、5 是质数 D. 程序报 SyntaxError,因为 for 不能与 else 搭配

第 4 题:可变默认参数的陷阱【应用·第 4 章】

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item('苹果'))
print(add_item('牛奶'))

输出是什么?

A. 第一行 ['苹果'],第二行 ['牛奶'] B. 第一行 ['苹果'],第二行 ['苹果', '牛奶'] C. 两行都是 [] D. 抛 TypeError:列表不能作为默认参数

第 5 题:re.match 与 re.search 的区别【应用·第 5 章】

import re

text = 'ID=42 客户=张三'
m = re.match(r'客户', text)
s = re.search(r'客户', text)
print(m)
print(s.group())

输出是什么?

A. 第一行 None,第二行 客户 B. 两行都是 None C. 两行都是 客户 D. 抛 AttributeError:‘NoneType’ object has no attribute ‘group’

第 6 题:浅拷贝的修改传播【理解·第 6 章】

import copy

nums = [[1, 2], [3, 4]]
cloned = copy.copy(nums)
cloned[0][0] = 99
print(nums)

输出是什么?

A. [[1, 2], [3, 4]] B. [[99, 2], [3, 4]] C. [[99, 2], [99, 4]] D. 抛 RuntimeError:无法复制嵌套列表

第 7 题:finally 与 return 的执行顺序【应用·第 7 章】

def read_size(path):
    try:
        with open(path, encoding='utf-8') as f:
            return len(f.read())
    except FileNotFoundError:
        return -1
    finally:
        print('清理完成')

print(read_size('no_such_file_xyz_2026.txt'))

假设该文件不存在,输出是什么?

A. 只打印 -1 B. 先打印 清理完成,再打印 -1 C. 先打印 -1,再打印 清理完成 D. 抛 FileNotFoundError,程序崩溃

第 8 题:super() 的调用链【理解·第 8 章】

class A:
    def greet(self):
        print('A')

class B(A):
    def greet(self):
        print('B')
        super().greet()

class C(B):
    pass

C().greet()

输出是什么?

A. 只打印 B B. 只打印 A C. 先打印 B,再打印 A D. 递归调用直到 RecursionError

第 9 题:basicConfig 的级别门槛【应用·第 9 章】

import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')
logging.debug('正在连接数据库')
logging.info('服务启动完成')

输出是什么?

A. 两行都输出 B. 只输出 DEBUG 正在连接数据库 C. 只输出 INFO 服务启动完成 D. 什么都不输出

第 10 题:timeit 的返回值【应用·第 10 章】

import timeit

t = timeit.timeit(stmt='sum(range(1000))', number=10000)
print(t)

关于 t 的说法,哪个正确?

A. t 是整数,单位是毫秒,表示单次执行耗时 B. t 是 float,单位是秒,表示连续执行 10000 次的总耗时 C. t 是 float,单位是秒,表示单次执行的平均耗时 D. t 是格式化的字符串,形如 "0.10s"

第 11 题:未绑定的局部变量【应用·第 10 章】

def compute(flag):
    if flag:
        total = 100
    return total + 1

print(compute(False))

输出是什么?

A. 打印 101 B. 抛 NameError:name ‘total’ is not defined C. 抛 UnboundLocalError,报错信息包含 cannot access local variable ‘total’ D. 打印 None

第 12 题:is 与 == 的边界【综合·第 10 章】

def literal_case():
    a = 257
    b = 257
    return a is b

def runtime_case():
    a = 257
    b = int('257')
    return a is b

print(literal_case(), runtime_case())

输出是什么?

A. True True B. False False C. True False D. False True

点击查看答案与逐题解析

答案:A C B B A B B C C B C C

第 1 题(A):venv 是标准库自带的工具(第 1 章第 4 篇),以当前解释器为基底建立隔离的 site-packages 与脚本目录,不重新下载安装包;激活只是改写 PATH 与相关环境变量,让 python 优先指向虚拟环境,而不是手动改系统 PATH;选项 B 描述的是”不激活也能用”的另一种方式——直接调用 .venv 里的解释器路径,无需改 PATH。第 2 题(C):.2f 表示保留两位小数并四舍五入,3.14159 → 3.14(第 2 章第 5 篇)。第 3 题(B):for...else 的 else 只在循环”没有被 break 打断地正常结束”时执行,且这里 break 只跳出内层循环:2 的内层循环为空、3 找不到约数,走 else 打印质数;4 被 2 整除,先打印合数再 break,跳过 else;5 是质数(第 3 章第 5、6 篇)。第 4 题(B):默认参数在函数定义时求值一次,items=[] 是同一个列表对象被所有调用共享,第二次调用时它已经装着 '苹果'(第 4 章第 3 篇)。修复惯例是 items=None 后在函数内新建。第 5 题(A):re.match 只从字符串开头匹配,客户 不在开头所以返回 None;re.search 在整个字符串里找第一次出现的位置并返回匹配对象(第 5 章第 5 篇)。第 6 题(B):copy.copy 是浅拷贝,只复制外层列表,两个内层子列表仍是同一批对象,改 cloned[0][0] 等于改 nums[0][0];要完全独立需要 copy.deepcopy(第 6 章第 5 篇)。第 7 题(B):finally 在 return 真正把值交出去之前执行——输出顺序是”清理完成”先出现,然后 print(read_size(...)) 打印返回值 -1;异常被 except 接住,不会崩溃(第 4 章第 8 篇的异常体系 + 第 7 章第 2 篇的 with 语句)。第 8 题(C):C 没有重写 greet,调用的是 B 的实现;super().greet() 沿方法解析顺序(MRO)找到父类 A 的实现,先打印 B 再打印 A(第 8 章第 4 篇)。第 9 题(C):basicConfig(level=logging.INFO) 把默认门槛从 WARNING 调到 INFO,DEBUG 低于门槛被过滤(第 9 章第 4 篇)。第 10 题(B):timeit.timeit 返回”连续执行 number 次的总秒数”,float 类型;单次平均等于 t 除以 number。本机实测 sum(range(1000)) 执行 10000 次约 0.10 秒,数值随机器浮动(本批第 6 篇)。第 11 题(C):函数体内被赋值过的名字都是局部变量,total 只在 flag 为真时被赋值,为假时访问它属于”从未绑定”——Python 3.11 起的报错文案就是 cannot access local variable ‘total’ where it is not associated with a value,3.10 及更早版本文案是 referenced before assignment,抛的都是 UnboundLocalError 而非 NameError(本批第 7 篇)。第 12 题(C):is 比较对象身份而不是值。CPython 对 -5 到 256 的小整数有全局缓存;更大的整数通常按需创建新对象,但同一函数作用域内两个相同的整数字面量会被编译器合并进同一常量表,所以 literal_case 为 True;int('257') 在运行时新建对象,所以 runtime_case 为 False。这些是 CPython 的实现细节,语言规范并不保证——判断值相等永远用 ==(第 2 章第 7 篇 + 本批第 4 篇)。

编程题(共 4 题,附可运行参考解答与实测输出)

编程题 1:带 CLI 与测试与日志的电话簿 mini 版

题目:实现 phonebook.py:三个子命令 add NAME PHONE、list、find KEYWORD;数据以 JSON 形式持久化到脚本同目录的 contacts.json;发送日志记录”新增联系人”事件(INFO 级别);把核心逻辑 load、save、add、find 设计成可注入文件路径的函数(不依赖命令行),并配套 test_phonebook.py,用 unittest 覆盖:存取往返(保存后能原样读回)、add 返回最新总数、按姓名与按电话查找、数据文件不存在时返回空列表。规模控制在 40-60 行以内,这正是第八篇里程碑项目《通讯录管理系统》的缩减骨架。考察点:第 9 章 argparse 子命令与退出码、第 7 章 JSON 文件读写与 with、第 4 章函数封装、第 9 章 logging 与 unittest、第 6 章列表与字典。

参考解答(phonebook.py,本机实测 52 行):

"""电话簿 mini 版:add/list/find 三个子命令 + JSON 持久化 + 日志。"""
import argparse
import json
import logging
import sys
from pathlib import Path

DEFAULT_FILE = Path(__file__).with_name('contacts.json')
logging.basicConfig(level=logging.INFO, format='%(levelname)s %(message)s')

def load(path=DEFAULT_FILE):
    if not path.exists():
        return []
    with open(path, encoding='utf-8') as f:
        return json.load(f)

def save(contacts, path=DEFAULT_FILE):
    with open(path, 'w', encoding='utf-8') as f:
        json.dump(contacts, f, ensure_ascii=False, indent=2)

def add(contacts, name, phone):
    contacts.append({'name': name, 'phone': phone})
    return len(contacts)

def find(contacts, keyword):
    return [c for c in contacts
            if keyword in c['name'] or keyword in c['phone']]

def main(argv=None):
    parser = argparse.ArgumentParser(prog='phonebook')
    sub = parser.add_subparsers(dest='command', required=True)
    sub.add_parser('list', help='列出全部联系人')
    p_add = sub.add_parser('add', help='新增联系人')
    p_add.add_argument('name')
    p_add.add_argument('phone')
    p_find = sub.add_parser('find', help='按关键字查找')
    p_find.add_argument('keyword')
    args = parser.parse_args(argv)
    contacts = load()
    if args.command == 'add':
        n = add(contacts, args.name, args.phone)
        save(contacts)
        logging.info('已添加 %s,当前共 %d 人', args.name, n)
    elif args.command == 'list':
        for i, c in enumerate(contacts, 1):
            print(f'{i}. {c["name"]} {c["phone"]}')
    elif args.command == 'find':
        for c in find(contacts, args.keyword):
            print(f'{c["name"]} {c["phone"]}')
    return 0

if __name__ == '__main__':
    sys.exit(main())

配套测试(test_phonebook.py):

"""电话簿核心逻辑测试:不触碰真实数据文件。"""

import tempfile
import unittest
from pathlib import Path

from phonebook import add, find, load, save


class PhonebookTest(unittest.TestCase):
    def setUp(self):
        self.dir = tempfile.TemporaryDirectory()
        self.path = Path(self.dir.name) / 'contacts.json'

    def tearDown(self):
        self.dir.cleanup()

    def test_add_and_save_load_roundtrip(self):
        contacts = []
        add(contacts, '张三', '13800138000')
        save(contacts, self.path)
        self.assertEqual(load(self.path), [
            {'name': '张三', 'phone': '13800138000'}])

    def test_add_returns_new_count(self):
        contacts = [{'name': '张三', 'phone': '13800138000'}]
        self.assertEqual(add(contacts, '李四', '13900139000'), 2)

    def test_find_by_name_and_phone(self):
        contacts = [
            {'name': '张三', 'phone': '13800138000'},
            {'name': '李四', 'phone': '13900139000'},
        ]
        self.assertEqual(len(find(contacts, '三')), 1)
        self.assertEqual(find(contacts, '1390')[0]['name'], '李四')

    def test_load_missing_file_returns_empty(self):
        self.assertEqual(load(self.path), [])


if __name__ == '__main__':
    unittest.main(verbosity=2)

本机实测命令演示($ 开头为输入的命令,其后为输出):

$ python phonebook.py add 张三 13800138000
INFO 已添加 张三,当前共 1 人
$ python phonebook.py add 李四 13900139000
INFO 已添加 李四,当前共 2 人
$ python phonebook.py list
1. 张三 13800138000
2. 李四 13900139000
$ python phonebook.py find 李
李四 13900139000
$ python phonebook.py find 1390
李四 13900139000
$ python -m unittest test_phonebook -v
test_add_and_save_load_roundtrip (test_phonebook.PhonebookTest.test_add_and_save_load_roundtrip) ... ok
test_add_returns_new_count (test_phonebook.PhonebookTest.test_add_returns_new_count) ... ok
test_find_by_name_and_phone (test_phonebook.PhonebookTest.test_find_by_name_and_phone) ... ok
test_load_missing_file_returns_empty (test_phonebook.PhonebookTest.test_load_missing_file_returns_empty) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.006s

OK

持久化文件 contacts.json 的内容(indent=2 + ensure_ascii=False,中文原样保存):

[
  {
    "name": "张三",
    "phone": "13800138000"
  },
  {
    "name": "李四",
    "phone": "13900139000"
  }
]

关键点:核心函数全部以”数据列表 + 可选路径”为参数,测试因此不碰真实文件(用 tempfile 造临时目录);find 是返回新列表的纯查询,不修改原数据;main 返回 0 并通过 sys.exit(main()) 交给 Shell。四个测试恰好覆盖”成功路径、返回值、查询语义、缺失文件”四类行为,这正是第八篇里程碑项目总结的测试分层思路。

编程题 2:日志分析——正则、容器与文件的组合

题目:把下面的 7 行内容保存为 access.log(第 7 章讲过的 UTF-8 文本文件),编写 log_analyzer.py:函数 count_levels(path) 统计每一行出现的级别(DEBUG/INFO/WARNING/ERROR)次数,返回字典;函数 top_users(path, top=3) 统计 user=xxx 的用户出现次数,返回出现最多的前 3 名(元组列表);main() 调用两者并打印结果。考察点:第 5 章正则的 search 与分组、第 6 章 collections.Counter、第 7 章 with 逐行读文件、第 4 章函数封装。

fixture access.log(题目自带,7 行):

2026-09-04 10:00:01 INFO user=张三 action=login message=登录成功
2026-09-04 10:00:05 DEBUG user=李四 action=query message=缓存未命中
2026-09-04 10:00:11 WARNING user=王五 action=pay message=余额不足
2026-09-04 10:00:20 INFO user=张三 action=query message=查询订单
2026-09-04 10:00:33 ERROR user=李四 action=pay message=支付超时
2026-09-04 10:00:40 INFO user=张三 action=logout message=正常退出
2026-09-04 10:00:52 DEBUG user=王五 action=query message=缓存未命中

参考解答(log_analyzer.py):

"""access.log 分析:按级别计数 + 找出最活跃的用户。"""

import re
from collections import Counter

LEVEL_PATTERN = re.compile(r'\b(DEBUG|INFO|WARNING|ERROR)\b')
USER_PATTERN = re.compile(r'user=([\u4e00-\u9fff\w]+)')


def count_levels(path):
    counter = Counter()
    with open(path, encoding='utf-8') as f:
        for line in f:
            m = LEVEL_PATTERN.search(line)
            if m:
                counter[m.group(1)] += 1
    return dict(counter)


def top_users(path, top=3):
    counter = Counter()
    with open(path, encoding='utf-8') as f:
        for line in f:
            m = USER_PATTERN.search(line)
            if m:
                counter[m.group(1)] += 1
    return counter.most_common(top)


def main():
    levels = count_levels('access.log')
    print('级别分布:', levels)
    print('活跃用户 TOP3:', top_users('access.log'))


if __name__ == '__main__':
    main()

本机实测输出:

$ python log_analyzer.py
级别分布: {'INFO': 3, 'DEBUG': 2, 'WARNING': 1, 'ERROR': 1}
活跃用户 TOP3: [('张三', 3), ('李四', 2), ('王五', 2)]

关键点:re.search 负责”在一行里找级别关键词”,分组括号让 m.group(1) 直接取出用户名(第 5 章);Counter 的 most_common(top) 一次完成计数与排序(第 6 章);for line in f 配合 with 逐行读文件,7 行日志不会整块进内存(第 7 章)。想升级的话,把 message=登录成功 也解析出来,就是第五章日志解析案例的完全形态。

编程题 3:学生成绩单——OOP、校验与 JSON 的配合

题目:实现 gradebook.py:类 Student 持有姓名与课程成绩字典,用 property 暴露只读的 total(总分)与 average(均分,无成绩时返回 0.0);类 GradeSheet 负责把成绩单持久化为 JSON(文件不存在时自动从空开始),提供 add_student(重名抛 ValueError)、add_score(成绩不在 0-100 抛 ValueError)、class_average(班级均分);__main__ 演示录入两名学生、保存并打印均分。配套 test_gradebook.py 用 unittest 覆盖:总分与均分计算、空成绩均分为 0.0、重名学生抛异常、越界成绩抛异常、保存后能原样读回。考察点:第 8 章类与 property、第 7 章异常与 JSON、第 9 章 unittest。

参考解答(gradebook.py,本机实测 60 行):

"""成绩单:Student + GradeSheet,JSON 持久化,带校验。"""
import json
from pathlib import Path

MAX_SCORE = 100

class Student:
    def __init__(self, name, scores=None):
        self.name = name
        self.scores = dict(scores or {})
    @property
    def total(self):
        return sum(self.scores.values())
    @property
    def average(self):
        if not self.scores:
            return 0.0
        return self.total / len(self.scores)

class GradeSheet:
    def __init__(self, path):
        self.path = Path(path)
        self.students = {}
        if self.path.exists():
            self.load()
    def load(self):
        with open(self.path, encoding='utf-8') as f:
            raw = json.load(f)
        self.students = {name: Student(name, scores)
                         for name, scores in raw.items()}
    def save(self):
        raw = {name: s.scores for name, s in self.students.items()}
        with open(self.path, 'w', encoding='utf-8') as f:
            json.dump(raw, f, ensure_ascii=False, indent=2)
    def add_student(self, student):
        if student.name in self.students:
            raise ValueError(f'学生 {student.name} 已存在')
        self.students[student.name] = student
    def add_score(self, name, course, score):
        if not 0 <= score <= MAX_SCORE:
            raise ValueError(f'成绩 {score} 超出 0-100 范围')
        self.students[name].scores[course] = score
    def class_average(self):
        if not self.students:
            return 0.0
        return sum(s.average for s in self.students.values()) / len(self.students)

if __name__ == '__main__':
    sheet = GradeSheet('grades.json')
    sheet.add_student(Student('张三'))
    sheet.add_student(Student('李四'))
    sheet.add_score('张三', 'Python', 92)
    sheet.add_score('张三', '数学', 88)
    sheet.add_score('李四', 'Python', 78)
    sheet.save()
    print('张三均分:', sheet.students['张三'].average)
    print('班级均分:', sheet.class_average())

配套测试(test_gradebook.py,注意用 assertRaises 同时验证”异常被抛出”与”数据未被破坏”):

"""成绩单核心逻辑测试。"""

import tempfile
import unittest
from pathlib import Path

from gradebook import GradeSheet, Student


class GradeSheetTest(unittest.TestCase):
    def setUp(self):
        self.dir = tempfile.TemporaryDirectory()
        self.path = Path(self.dir.name) / 'grades.json'

    def tearDown(self):
        self.dir.cleanup()

    def test_average_and_total(self):
        s = Student('张三', {'Python': 92, '数学': 88})
        self.assertEqual(s.total, 180)
        self.assertEqual(s.average, 90.0)

    def test_average_empty_scores_is_zero(self):
        self.assertEqual(Student('王五').average, 0.0)

    def test_duplicate_student_raises(self):
        sheet = GradeSheet(self.path)
        sheet.add_student(Student('张三'))
        with self.assertRaises(ValueError):
            sheet.add_student(Student('张三'))

    def test_out_of_range_score_raises(self):
        sheet = GradeSheet(self.path)
        sheet.add_student(Student('张三'))
        with self.assertRaises(ValueError):
            sheet.add_score('张三', 'Python', 101)

    def test_save_and_load_roundtrip(self):
        sheet = GradeSheet(self.path)
        sheet.add_student(Student('张三', {'Python': 92}))
        sheet.save()
        loaded = GradeSheet(self.path)
        self.assertEqual(loaded.students['张三'].scores, {'Python': 92})


if __name__ == '__main__':
    unittest.main(verbosity=2)

本机实测输出(demo 与测试):

$ python gradebook.py
张三均分: 90.0
班级均分: 84.0
$ python -m unittest test_gradebook -v
test_average_and_total (test_gradebook.GradeSheetTest.test_average_and_total) ... ok
test_average_empty_scores_is_zero (test_gradebook.GradeSheetTest.test_average_empty_scores_is_zero) ... ok
test_duplicate_student_raises (test_gradebook.GradeSheetTest.test_duplicate_student_raises) ... ok
test_out_of_range_score_raises (test_gradebook.GradeSheetTest.test_out_of_range_score_raises) ... ok
test_save_and_load_roundtrip (test_gradebook.GradeSheetTest.test_save_and_load_roundtrip) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.008s

OK

grades.json 实际内容:

{
  "张三": {
    "Python": 92,
    "数学": 88
  },
  "李四": {
    "Python": 78
  }
}

关键点:property 把”读取”封装成方法却保留属性的调用语法,均分计算因此不能在别处被绕过(第 8 章第 3 篇);异常在”数据被修改之前”抛出,测试里的 assertRaises 之后数据保持原样——失败路径的副作用为零是比”抛了没抛”更严格的检验;dict(scores or {}) 顺手做了一层浅拷贝,防止外部字典被共享修改。

编程题 4:斐波那契——递归、缓存与测量的三方对照

题目:编写 fib_perf.py,实现三种求第 n 个斐波那契数的函数:朴素递归 fib_naive(直接按定义递归)、make_fib_cached(闭包持有字典 cache,第 4 章闭包 + 第 6 章字典,返回一个记忆化递归函数)、循环版 fib_iterative(两个变量滚动)。先用断言验证三者结果一致,再用 timeit 测量:朴素递归执行 1 次、缓存版本执行 100000 次、循环版本执行 100000 次各自耗时,打印对比。考察点:第 4 章递归与闭包、第 6 章字典充当缓存、第 7 章断言、本批第 6 篇 timeit 的正确用法(number 参数与总耗时语义)。

参考解答(fib_perf.py):

"""斐波那契:朴素递归 vs 字典缓存,用 timeit 测量差距。"""

import timeit


def fib_naive(n):
    if n < 2:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)


def make_fib_cached():
    cache = {0: 0, 1: 1}

    def fib(n):
        if n not in cache:
            cache[n] = fib(n - 1) + fib(n - 2)
        return cache[n]

    return fib


def fib_iterative(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a


if __name__ == '__main__':
    n = 30
    fib_cached = make_fib_cached()
    assert fib_naive(n) == fib_cached(n) == fib_iterative(n)
    print(f'fib({n}) = {fib_cached(n)},三种实现结果一致')

    t_naive = timeit.timeit(lambda: fib_naive(n), number=1)
    t_cache = timeit.timeit(lambda: fib_cached(n), number=100000)
    t_iter = timeit.timeit(lambda: fib_iterative(n), number=100000)
    print(f'朴素递归 1 次:{t_naive:.3f} 秒')
    print(f'缓存递归 100000 次:{t_cache:.3f} 秒')
    print(f'循环 100000 次:{t_iter:.3f} 秒')

本机实测输出(计时数值随机器浮动,看量级即可):

$ python fib_perf.py
fib(30) = 832040,三种实现结果一致
朴素递归 1 次:0.109 秒
缓存递归 100000 次:0.009 秒
循环 100000 次:0.088 秒

关键点:朴素递归把大量子问题重复计算,fib(30) 一共执行约 269 万次函数调用(本机实测计数 2692537 次),慢的根源是”同样的参数被反复求解”;字典缓存让每个 n 只算一次,于是执行 100000 次反而比朴素版执行 1 次快一个量级;循环版连递归都不需要,是三者中最省的。三种实现的正确性由同一句 assert 钉住——先验证正确,再测量性能,这正是本批第 6 篇”性能初体验”的完整决策流程:先正确、再优化、后验证。

高频错因速查

  • 是 12 个变量 3.14 不是 3.1416:.2f 保留两位小数;与此相对 {pi} 不加说明符就是 3.14159(第 2 题)。
  • for…else 不是”配套成对”语法糖:else 只管”循环没被 break 打断”这一种结局,break 打断的是它所在的那层循环(第 3 题)。
  • 默认参数只求值一次:可变默认值(列表、字典)会被所有调用共享——排查”上次的结果跑到了这次”优先怀疑它(第 4 题)。
  • match 只能匹配开头,search 才全文查找:拿不准就两个都写出来跑一遍(第 5 题)。
  • 浅拷贝只保护第一层:嵌套容器要 deepcopy;测试类场景里”改内层”最容易骗过眼睛(第 6 题)。
  • finally 先于函数返回执行:清理类操作(关文件、打日志、发信号)放 finally 永远会被执行(第 7 题)。
  • super() 是委托不是调用自己:沿 MRO 找”下一个”实现,理解它就不会问”为什么不会死循环”(第 8 题)。
  • logger 级别与 Handler 级别是两道门槛:basicConfig 只调默认 Handler 的门槛;本条见第 9 章第 4、5 篇(第 9 题)。
  • timeit 返回的是总耗时:要单次平均自己除以 number;只需要计时器重建而不是比较时,number 要选得让总时长落在秒级(第 10 题)。
  • UnboundLocalError 不等于 NameError:函数内只要出现赋值,名字就归局部管;“条件分支里才赋值”是最常见成因(第 11 题)。
  • is 是身份不是值:小整数缓存与常量合并都是 CPython 的实现细节,跨实现不成立;数值比较一律用 ==(第 12 题)。

课程 01 学习自检总清单

完成本测验后,请逐项确认你能独立做到(每章两项,共 20 项):

第 1 章 环境搭建与运行原理

  • 用 python -m venv 创建并激活虚拟环境,解释 pip 与解释器的关系,能安装与卸载第三方包。
  • 说出 CPython 把源码翻译并执行的流程,能在 REPL 与 python -m pdb 里做单步调试。

第 2 章 变量、类型与运算符

  • 用 f-string 与 format 说明符控制数字格式(小数位、千分位),理解浮点数误差并能够规避。
  • 解释 and/or/not 的短路与真值判定,说清 == 与 is 的适用场景(对照第 12 题)。

第 3 章 流程控制

  • 用 if/elif/else、while、for、break/continue 写出正确分支,讲清 for…else 的语义(对照第 3 题)。
  • 用推导式一行构造列表、字典与集合,并能说出它们与循环的等价关系。

第 4 章 函数

  • 说出位置参数、默认值、关键字参数与可变参数的接收规则,解释作用域与 global/闭包(对照第 4 题)。
  • 给每个自写函数配 docstring;能解释可变默认参数的陷阱并写出修复写法。

第 5 章 字符串与正则

  • 熟练使用切片与常用字符串方法,记住 str 不可变(对照第 2 题)。
  • 用 re 模块完成 search/match/findall 与分组提取,能解析真实格式的日志文本(对照第 5 题与编程题 2)。

第 6 章 列表、元组与字典

  • 熟练容器增删查改与嵌套访问,解释引用语义并区分 copy 与 deepcopy(对照第 6 题)。
  • 用 collections.Counter 完成计数与 top-N 统计,用它写出词频统计(对照编程题 2)。

第 7 章 文件与异常处理

  • 用 with 与 open 读写 UTF-8 文本,用 pathlib 组装路径;说出 with 等价于 try/finally 的哪一部分(对照第 7 题)。
  • 按”具体异常优先”写出 try/except/finally,理解异常继承体系并会用 assert。

第 8 章 面向对象

  • 定义类、init 与实例方法,用 property 封装只读属性或校验(对照编程题 3)。
  • 解释继承、super 与 MRO,能说出”多态”在 Python 里的运行时含义(对照第 8 题)。

第 9 章 命令行工具

  • 用 argparse 组织位置参数与子命令,用 sys.exit 控制退出码并说明约定(对照编程题 1)。
  • 配置 logging 并解释 logger 与 Handler 两级门槛;用 unittest 组织测试用例与断言(对照第 9 题)。

第 10 章 阶段测验与查漏补缺

  • 能识别并应对全书高频坑:可变默认参数、is 误用、浅拷贝传播、未绑定局部变量、正则 match/search 混淆(对照第 4/12/6/11/5 题)。
  • 会用 timeit 做正确性优先的性能测量,能按”先正确、再优化、后验证”决策;能独立完成里程碑项目式的 CLI+存储+测试+日志小工具(对照编程题 1/4)。

小结

十二道选择题与四道编程题把课程 01 阶段一的十章串成了一张知识网:语法(2/3 章)是砖,函数与容器(4/6 章)是梁,字符串与文件(5/7 章)是门窗,面向对象(8 章)是房间布局,命令行与工程化(9 章)是把房子交付出去的施工标准,性能与调试(10 章)是入住后的维护手册。测验的意义不在分数,而在把”我大概会了”变成”我能证明我会了”——任何一项清单打不上勾,就回到对应的篇章重读。全部打勾之后,按照第九篇的路线图,先补齐课程 01 剩余的 11-30 章,然后沿着算法线、系统线、方向线继续前进——你的 Python 之旅才刚刚过了第一座桥。

练习与思考题(挑战三道)

  1. 把第 12 题的实验推广:在同一个函数里写 a = 'hello'、b = 'hello',a is b 是多少?把它与 ''.join(['he', 'llo']) 比较,结果如何?用自己的话解释”常量合并”与”运行时拼接”的分界(答案导向:同一作用域的两个相同字符串字面量因常量合并而 is 为 True;运行时拼接出的新对象与之不相等——动手在 REPL 里验证,并说一说为什么这种依赖实现细节的写法在工程里要避免)。
  2. 给你的电话簿加一个 remove NAME 子命令:删除第一个姓名匹配的联系人并保存,给出”找不到时”的错误处理方案,并补一个对应的 unittest(答案导向:仿照 add 写 remove 纯函数返回是否删除成功(True/False 约定),命令行分支据此打印信息并返回非零退出码;测试覆盖”删除成功返回列表变化”与”找不到不抛异常返回 False”两种路径;可选再覆盖删除后保存读回)。
  3. 用 timeit 比较 for i in range(10000): s += str(i) 与 ''.join(str(i) for i in range(10000)) 的耗时,解释差异来自哪里(答案导向:str 不可变,+= 每次生成全新字符串导致总代价接近平方级;join 先收集再一次性拼接是线性级;本机实测 join 快一个量级——这正是”先想明白再优化”的验证题)。