## 引言
本章已经带你走过了函数的十个重要主题:从 `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 程序。记住:测验的目的不是分数,而是发现知识盲区。