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

第四章综合测验:函数与编程范式

## 引言 本章已经带你走过了函数的十个重要主题:从 `def` 的基本语法(第 1 篇)到作用域与命名空间(第 2 篇),从可变参数(第 3 篇)到 lambda 与高阶函数(第 4 篇),从装饰器(第 5 篇)到递归(第 6 篇),从类型注解(第 7 篇)到异常处理(第 8 篇),再到上下文管理器(第 9 篇)。本篇作为本章的收官之作,通过选择题、编程题和综合任务三种形式,帮你检验学习成果、发现知识盲点。 本篇的题目严格覆盖第 1 篇至第 9 篇的全部知识点,不引入类装饰器进阶、异步等后续章节的内容。所有代码均基于 CPython 3.10+ 的真实行为。建议先独立完成,再对照参考答案。 在开始之前,请先回顾一下本章的核心脉络:**函数是 Python 代码组织的基石**。从最基础的 `def` 定义,到参数传递的四种方式(位置、关键字、默认值、可变参数),再到作用域规则(LEGB)、闭包雏形、lambda 表达式、装饰器(语法糖的本质是函数嵌套调用)、递归(函数调用自身)、类型注解(静态检查工具 mypy)、异常处理(try/except/finally/else),最后到上下文管理器(with 语句的底层协议)。这九个主题层层递进,构成了 Python 函数式编程的完整基础。 下面的测验将检验你对这些知识点的掌握程度。选择题侧重概念辨析,每题都对应一个常见的理解误区;编程题侧重动手实现,要求你从零写出完整代码;综合任务侧重工程应用,模拟真实项目中常见的配置读取场景。请尽量在不看答案的情况下独立完成,做完后再对照解析查漏补缺。对于做错的题目,不要只看答案——要回到对应篇章重新理解原理,这才是最有效的复习方式。 ## 选择题(共 8 题,每题只有一个正确答案) ### 第 1 题:默认参数陷阱 ```python def append_to(item, target=[]): target.append(item) return target a = append_to(1) b = append_to(2) print(b) ``` 输出是什么? A. `[1]` B. `[2]` C. `[1, 2]` D. 报错 ### 第 2 题:作用域与 LEGB ```python x = 10 def outer(): x = 20 def inner(): x = 30 print(x) inner() print(x) outer() print(x) ``` 输出是什么? A. `30 20 10` B. `30 30 30` C. `20 20 10` D. `30 20 30` ### 第 3 题:解包与 *args/**kwargs ```python def summarize(label, *args, **kwargs): print(f'{label}: args={args}, kwargs={kwargs}') summarize('test', 1, 2, 3, name='Alice', age=25) ``` 输出是什么? A. `test: args=(1, 2, 3), kwargs={'name': 'Alice', 'age': 25}` B. `test: args=[1, 2, 3], kwargs={'name': 'Alice', 'age': 25}` C. `test: args=(1, 2, 3, 'name', 'Alice', 'age', 25), kwargs={}` D. 报错 ### 第 4 题:lambda 与 sorted ```python data = [('apple', 3), ('banana', 1), ('cherry', 2)] result = sorted(data, key=lambda x: x[1]) print(result) ``` 输出是什么? A. `[('apple', 3), ('cherry', 2), ('banana', 1)]` B. `[('banana', 1), ('cherry', 2), ('apple', 3)]` C. `[('banana', 1), ('apple', 3), ('cherry', 2)]` D. 报错 ### 第 5 题:装饰器与 functools.wraps ```python from functools import wraps def logger(func): @wraps(func) def wrapper(*args, **kwargs): print(f'calling {func.__name__}') return func(*args, **kwargs) return wrapper @logger def add(a, b): return a + b print(add.__name__) print(add(1, 2)) ``` 输出是什么? A. `wrapper` 和 `3` B. `add` 和 `3` C. `wrapper` 和报错 D. `add` 和报错 ### 第 6 题:递归与回溯 ```python def factorial(n): if n <= 1: return 1 return n * factorial(n - 1) print(factorial(5)) ``` 输出是什么? A. `120` B. `24` C. `6` D. 报错(递归深度超限) ### 第 7 题:类型注解与 Optional ```python from typing import Optional, List def find(items: List[int], target: int) -> Optional[int]: for i, v in enumerate(items): if v == target: return i return None print(find([10, 20, 30], 20)) print(find([10, 20, 30], 99)) ``` 输出是什么? A. `1` 和 `None` B. `20` 和 `None` C. `1` 和 `0` D. 报错 ### 第 8 题:上下文管理器与异常传播 ```python class MyCM: def __enter__(self): print('enter') return self def __exit__(self, exc_type, exc_val, exc_tb): print(f'exit: {exc_type}') return False try: with MyCM(): raise ValueError('boom') except ValueError: print('caught') ``` 输出是什么? A. `enter exit: None caught` B. `enter exit: <class 'ValueError'> caught` C. `enter exit: ValueError caught` D. `enter exit: <class 'ValueError'>`(没有 caught) <details> <summary>点击查看答案</summary> **答案:C C A B B A A B** 第 1 题:默认参数在函数定义时求值一次,后续调用共享同一个列表对象,所以 `b` 包含了 `a` 追加的元素。选 C。这是一个经典的 Python 陷阱,根源在于函数体执行时默认参数只初始化一次。正确做法是使用 `None` 作为哨兵值:`def append_to(item, target=None): if target is None: target = []`。 第 2 题:LEGB 规则:inner() 先打印自己的局部变量 30,outer() 打印自己的局部变量 20,全局作用域打印 10。选 A。注意 `inner()` 中的 `print(x)` 使用的是 inner 自身的局部变量 x=30,而不是 outer 的 x=20 或全局的 x=10。这就是 LEGB 规则的体现:Local -> Enclosing -> Global -> Built-in。 第 3 题:`*args` 收集位置参数为元组,`**kwargs` 收集关键字参数为字典。选 A。注意 args 的类型是 tuple 而非 list,这是 Python 的设计选择,因为元组是不可变的,更符合「参数集合」的语义。 第 4 题:lambda 按每个元组的第二个元素(价格)排序,升序。选 B。`sorted()` 默认升序排列,lambda 表达式 `lambda x: x[1]` 提取每个元组的第二个元素作为排序键。 第 5 题:`@wraps(func)` 将原函数的 `__name__` 等属性复制到 wrapper 上,所以 `add.__name__` 是 `'add'`。选 B。如果不加 `@wraps`,`add.__name__` 将是 `'wrapper'`,这会导致调试和日志工具难以追踪实际执行的函数。 第 6 题:5! = 5 × 4 × 3 × 2 × 1 = 120。选 A。递归的深度为 5 层,远小于 CPython 默认的 1000 层限制,不会触发 RecursionError。 第 7 题:find 返回索引,20 在索引 1 处;99 不存在返回 None。选 A。`Optional[int]` 表示返回值可以是 int 或 None,这在类型检查器(如 mypy)中会被正确校验。 第 8 题:`exc_type` 是异常类本身(`<class 'ValueError'>`),不是字符串;`__exit__` 返回 False 所以异常继续传播,被外部捕获。选 B。注意输出中 `exit:` 后面显示的是类的字符串表示,而不是简单的 `ValueError` 字符串。理解这一点有助于正确编写自定义异常处理器。 </details> ## 编程题(共 3 题,附可运行参考解答) ### 编程题 1:带缓存的阶乘函数 实现一个函数 `memoized_factorial(n)`,使用字典缓存已计算的结果,避免重复递归。测试 `memoized_factorial(50)` 应在毫秒级完成。 **参考解答:** ```python def memoized_factorial(n, _memo=None): if _memo is None: _memo = {} if n in _memo: return _memo[n] if n <= 1: return 1 _memo[n] = n * memoized_factorial(n - 1, _memo) return _memo[n] print(memoized_factorial(50)) ``` 运行结果: ```text 30414093201713378043612608166064768844377641568960512000000000000 ``` ```text 关键点:`_memo` 使用默认参数 `None` 而非直接写 `{}`,避免可变默认参数的陷阱(第 1 篇)。每次调用独立初始化缓存字典,防止多次调用的状态污染。如果没有记忆化,直接递归计算 `factorial(50)` 需要 50 层栈帧,而有了缓存后只需要计算一次。当 n 增大到 1000 时,普通递归会触发 `RecursionError`,而记忆化版本同样只占用 O(n) 栈空间但避免了重复计算。这也是动态规划思想在 Python 中的最简实现。 ### 编程题 2:简单的计时装饰器 实现一个装饰器 `timer`,打印被装饰函数的执行时间(秒),精确到微秒。使用 `time.perf_counter()` 而非 `time.time()`,因为前者精度更高且不受系统时钟调整影响。 **参考解答:** ``` ```python import time from functools import wraps def timer(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start print(f'{func.__name__} took {elapsed:.6f}s ({elapsed * 1e6:.1f}us)') return result return wrapper @timer def compute(): s = 0 for i in range(1000000): s += i return s compute() ``` 运行结果(时间因机器而异): ```text compute took 0.033995s (33995.0us) ``` ```text 关键点:必须使用 `@wraps(func)` 保留原函数的 `__name__` 等元信息(第 5 篇)。`time.perf_counter()` 是 monotonic clock,不受系统时间调整影响,适合测量时间间隔。如果不使用 `@wraps`,被装饰函数的 `__name__` 会变成 `'wrapper'`,这在调试和日志记录时会造成困扰——你无法从堆栈追踪中直接看出是哪个函数在运行。这也是为什么在 Python 3.2 引入 `functools.wraps` 之后,它成为了编写装饰器的标准做法。 ### 编程题 3:安全读取配置文件的上下文管理器 实现一个 `ConfigReader` 上下文管理器,从 JSON 配置文件读取配置。要求: - `__enter__` 中加载配置并打印日志 `"Config loaded from {path}"`; - 文件不存在时抛出 `FileNotFoundError`; - JSON 格式错误时抛出 `ValueError` 并附带错误信息; - `__exit__` 中如有异常,打印警告日志; - 提供 `get(key, default=None)` 方法读取配置值。 **参考解答:** ``` ```python import json import logging logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') logger = logging.getLogger(__name__) class ConfigReader: def __init__(self, path): self.path = path self.data = {} def __enter__(self): try: with open(self.path, 'r', encoding='utf-8') as f: self.data = json.load(f) logger.info(f'Config loaded from {self.path}') except FileNotFoundError: logger.error(f'Config file not found: {self.path}') raise except json.JSONDecodeError as e: logger.error(f'Invalid JSON in {self.path}: {e}') raise ValueError(f'Invalid JSON: {e}') from e return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: logger.warning(f'Exception during config reading: {exc_type.__name__}') return False def get(self, key, default=None): return self.data.get(key, default) ``` 使用示例: ```python # 先写入测试配置 import json, tempfile, os config = {'host': 'localhost', 'port': 8080, 'debug': True} tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) json.dump(config, tmp) tmp.close() with ConfigReader(tmp.name) as cfg: print(f'host={cfg.get("host")}, port={cfg.get("port")}') os.remove(tmp.name) ``` 运行结果: ```text INFO: Config loaded from /var/folders/.../tmpXXXXXX.json host=localhost, port=8080 ``` ```text 关键点:综合运用了第 8 篇(异常处理)、第 9 篇(上下文管理器)和第 6 篇(函数式风格)的知识。注意 `raise ValueError(...) from e` 使用了异常链(第 8 篇),保留了原始异常的追踪信息。在 `__exit__` 中,我们通过检查 `exc_type` 是否非 None 来判断是否有异常发生,这是上下文管理器的标准写法。`get()` 方法使用了字典的默认参数功能,避免 KeyError——这是 Python 中处理可选配置项的惯用方式。此外,文件路径使用 `tempfile.NamedTemporaryFile` 创建临时文件,测试完成后手动删除,这是一种安全的测试模式,不会污染文件系统。 ## 综合任务:带日志和异常处理的配置读取器 **任务描述**:编写一个完整的配置读取工具,满足以下要求: 1. 使用上下文管理器(第 9 篇)管理配置文件的生命周期。 2. 支持 JSON 和 YAML 两种格式(如果 YAML 不可用则降级为纯 JSON)。 3. 使用装饰器(第 5 篇)记录每次读取操作的时间和日志。 4. 使用类型注解(第 7 篇)标注函数签名。 5. 对常见错误(文件不存在、格式错误、键缺失)给出清晰的错误信息。 6. 提供 `get_section(section_name, key, default=None)` 方法读取嵌套配置。 **验收标准**: - 代码可通过 `python -m py_compile` 编译检查。 - 使用真实配置文件运行不报错。 - 异常时日志清晰,不静默吞掉错误。 - 类型注解完整,符合 PEP 484 规范。 **参考实现(核心部分):** ``` ```python import json import logging import time from functools import wraps from typing import Any, Dict, Optional from contextlib import contextmanager logger = logging.getLogger(__name__) def log_operation(func): @wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start logger.info(f'{func.__name__} completed in {elapsed:.4f}s') return result return wrapper @contextmanager def config_reader(path: str): data: Dict[str, Any] = {} try: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) logger.info(f'Loaded config from {path}') yield data except FileNotFoundError: logger.error(f'Config not found: {path}') raise except json.JSONDecodeError as e: logger.error(f'Invalid JSON in {path}: {e}') raise def get_nested(data: Dict[str, Any], keys: list, default: Any = None) -> Any: current = data for key in keys: if not isinstance(current, dict) or key not in current: return default current = current[key] return current ``` 上述实现综合运用了本章的多个知识点:`log_operation` 装饰器使用 `@wraps` 保留函数元信息(第 5 篇);`config_reader` 使用 `@contextmanager` 简化上下文管理器创建(第 9 篇);所有函数签名都带有类型注解(第 7 篇);异常处理区分了文件不存在和 JSON 解析错误两种情况(第 8 篇)。`get_nested` 函数展示了如何用简单的循环实现深层嵌套配置的安全访问,避免 `KeyError`。 **测试代码:** ```python import json, tempfile, os # 创建测试配置文件 config = {'app': {'host': 'localhost', 'port': 8080}, 'debug': True} tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) json.dump(config, tmp) tmp.close() # 使用配置读取器 with config_reader(tmp.name) as cfg: print(f'host={cfg.get(\"app\", {}).get(\"host\")}') print(f'debug={cfg.get(\"debug\")}') os.remove(tmp.name) ``` 运行结果: ```text INFO: Loaded config from /var/folders/.../tmpXXXXXX.json host=localhost debug=True ``` ```text ### 工程实践中的上下文管理器模式 在实际项目中,上下文管理器不仅仅用于文件操作,还可以用于多种资源管理和流程控制场景。 **数据库连接管理**:这是上下文管理器最常见的应用场景之一。通过 `__enter__` 获取连接,`__exit__` 确保连接被正确关闭或回滚,即使在事务执行过程中发生异常也能保证数据一致性。 ```python class DatabaseTransaction: def __init__(self, connection): self.conn = connection def __enter__(self): self.conn.begin() # 开始事务 return self.conn def __exit__(self, exc_type, exc_val, exc_tb): if exc_type: self.conn.rollback() # 异常时回滚 else: self.conn.commit() # 正常时提交 return False # 不吞掉异常 # 使用示例 with DatabaseTransaction(db) as conn: conn.execute("INSERT INTO users VALUES (?, ?)", (name, email)) conn.execute("UPDATE accounts SET balance = balance - ?", (amount,)) ``` **性能测试与基准测量**:用上下文管理器包装性能测试代码,自动计算和报告执行时间。这种模式在性能优化和分析中非常有用。 **临时状态切换**:当需要在某段代码执行期间临时改变全局状态,并在结束后恢复原状时,上下文管理器是理想的选择。比如临时修改日志级别、切换数据库连接池、修改环境变量等。 **并发控制**:结合 `threading.Lock` 或 `asyncio.Lock`,上下文管理器可以简化锁的获取和释放,避免死锁和资源泄漏。 ### 上下文管理器与生成器的关系 理解上下文管理器,必须同时理解 Python 的生成器(generator)机制。`@contextmanager` 装饰器的实现原理就是基于生成器的 `yield` 语句。当你在 `with` 块中执行代码时,实际上是在调用生成器的 `send()` 方法——`yield` 左边的代码在 `__enter__` 之前执行,`yield` 右边的代码在 `with` 块结束后执行。 这种设计有一个重要后果:生成器只能被迭代一次,所以 `@contextmanager` 装饰的函数也只能被用作一次上下文管理器。如果你尝试在同一个生成器对象上再次调用 `send()`,会得到 `RuntimeError: generator already executing`。这也是为什么第 9 篇讨论重试逻辑时会遇到困难——重试需要多次进入和退出上下文,而生成器模型天然不支持这种模式。 深入理解这一机制有助于写出更正确的上下文管理器。当 `yield` 表达式被 `with` 块内的代码“发送”值时,这个值会成为 `yield` 表达式的结果;当 `with` 块结束或抛出异常时,Python 会调用生成器的 `throw()` 方法将异常注入到生成器内部。这就是为什么 `@contextmanager` 中的 `yield` 必须放在 `try/finally` 块中——只有这样才能捕获异常并正确执行清理代码。 ## 学习自检清单 完成本章学习后,请逐项确认你是否能独立完成以下操作: - [ ] 定义含默认参数、`*args`、`**kwargs` 的函数,并解释默认参数为何不应使用可变对象。 - [ ] 解释 LEGB 作用域规则,并能预测嵌套函数中变量的查找顺序。 - [ ] 编写一个简单的装饰器,正确使用 `functools.wraps` 保留原函数元信息。 - [ ] 实现递归函数,理解栈帧和回溯过程,能用记忆化优化递归性能。 - [ ] 为函数添加 `typing` 模块的类型注解,理解 `Optional`、`List`、`Dict` 的含义。 - [ ] 正确使用 `try/except/finally/else` 处理异常,理解异常链(`raise ... from ...`)。 - [ ] 实现一个类方式的上下文管理器(`__enter__` / `__exit__`),并理解异常传播机制。 - [ ] 使用 `contextlib` 的 `@contextmanager`、`closing`、`redirect_stdout`、`suppress` 解决实际问题。 - [ ] 理解为什么 `@contextmanager` 不适合实现重试逻辑(`yield` 只执行一次)。 - [ ] 阅读他人的 Python 代码时,能识别上下文管理器和装饰器的使用模式。 如果某一项无法确认,请回顾对应篇章重新练习。本章的核心思想是:**函数是 Python 代码组织的基石**,以上所有技能都是在此基础上构建的工程化能力。函数不仅是代码复用的工具,更是思维组织的方式——一个设计良好的函数应该有单一职责、清晰的输入输出、以及可测试性。好的函数名应该像动词短语一样描述行为(如 而非 ),参数列表应该精简,返回值的类型应该明确。掌握这些概念后,你将继续学习面向对象编程(第五章),届时你会发现函数式思维与面向对象思维可以很好地互补,两者结合才能写出真正健壮的 Python 程序。记住:测验的目的不是分数,而是发现知识盲区。