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

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

Python函数综合测验编程范式复习

引言

本章已经带你走过了函数的十个重要主题:从 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 题:默认参数陷阱

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

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

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

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

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 题:递归与回溯

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

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 题:上下文管理器与异常传播

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)

点击查看答案

答案: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 字符串。理解这一点有助于正确编写自定义异常处理器。

编程题(共 3 题,附可运行参考解答)

编程题 1:带缓存的阶乘函数

实现一个函数 memoized_factorial(n),使用字典缓存已计算的结果,避免重复递归。测试 memoized_factorial(50) 应在毫秒级完成。

参考解答:

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))

运行结果:

30414093201713378043612608166064768844377641568960512000000000000

关键点:`_memo` 使用默认参数 `None` 而非直接写 `{}`,避免可变默认参数的陷阱(第 1 篇)。每次调用独立初始化缓存字典,防止多次调用的状态污染。如果没有记忆化,直接递归计算 `factorial(50)` 需要 50 层栈帧,而有了缓存后只需要计算一次。当 n 增大到 1000 时,普通递归会触发 `RecursionError`,而记忆化版本同样只占用 O(n) 栈空间但避免了重复计算。这也是动态规划思想在 Python 中的最简实现。

### 编程题 2:简单的计时装饰器

实现一个装饰器 `timer`,打印被装饰函数的执行时间(秒),精确到微秒。使用 `time.perf_counter()` 而非 `time.time()`,因为前者精度更高且不受系统时钟调整影响。

**参考解答:**
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()

运行结果(时间因机器而异):

compute took 0.033995s (33995.0us)

关键点:必须使用 `@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)` 方法读取配置值。

**参考解答:**
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)

使用示例:

# 先写入测试配置
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)

运行结果:

INFO: Config loaded from /var/folders/.../tmpXXXXXX.json
host=localhost, port=8080

关键点:综合运用了第 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 规范。

**参考实现(核心部分):**
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。

测试代码:

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)

运行结果:

INFO: Loaded config from /var/folders/.../tmpXXXXXX.json
host=localhost
debug=True

工程实践中的上下文管理器模式

在实际项目中,上下文管理器不仅仅用于文件操作,还可以用于多种资源管理和流程控制场景。

数据库连接管理:这是上下文管理器最常见的应用场景之一。通过 __enter__ 获取连接,__exit__ 确保连接被正确关闭或回滚,即使在事务执行过程中发生异常也能保证数据一致性。

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 程序。记住:测验的目的不是分数,而是发现知识盲区。