Lab 14Part 6 Pythonic 与现代实践AI Level 1
函数式编程 · 团队项目启动
五道题练闭包、装饰器和作用域,然后是本学期最大的一件事——团队项目:两人一组,做一个简化版 NumPy。
截止:小作业 5 · 12 月 29 日 周二 23:59
本页目录
本次目标
- 亲手踩一遍可变默认参数和延迟绑定
- 写出能用的装饰器
- 用 LEGB 解释任何一个名字的来源
- 组队,启动团队项目
题目
14-1 两个陷阱
不运行,先在提交说明里预测下面每段的输出,再运行验证,标出你猜错的:
# A
def f(x, acc=[]):
acc.append(x)
return acc
print(f(1)); print(f(2)); print(f(3))
# B
funcs = [lambda: i for i in range(3)]
print([g() for g in funcs])
# C
def g(x, acc=None):
if acc is None:
acc = []
acc.append(x)
return acc
print(g(1)); print(g(2))
# D
funcs = [lambda i=i: i for i in range(3)]
print([g() for g in funcs])
然后用一句话解释:A 和 C 为什么不同?B 和 D 为什么不同?
14-2 计数装饰器
写一个装饰器 count_calls,记录函数被调用的次数:
@count_calls
def greet(name: str) -> str:
return f"你好,{name}"
greet("张三"); greet("李四")
print(greet.call_count) # 2
print(greet.__name__) # 必须还是 "greet"
print(greet.__doc__) # 原文档必须保留
要求用 functools.wraps。
14-3 带参数的装饰器
写一个 retry(times),让被装饰的函数失败时最多重试 times 次,全部失败才抛出最后一次的异常:
import random
@retry(times=5)
def flaky() -> str:
if random.random() < 0.7:
raise ValueError("失败了")
return "成功"
print(flaky())
要求:
- 每次重试打印一行提示
- 用固定种子测试,保证结果可复现(L12)
14-4 计时并缓存
写一个装饰器 timed_cache,同时做两件事:缓存结果,并打印每次调用是命中缓存还是真算了。
@timed_cache
def slow_square(n: int) -> int:
time.sleep(0.1)
return n * n
slow_square(4) # 计算,耗时 0.1 秒
slow_square(4) # 命中缓存,几乎不耗时
slow_square(5) # 计算
在提交说明里回答:这个装饰器对什么样的函数不安全? 举一个具体例子。
14-5 LEGB 追踪
对下面这段代码,在提交说明里逐个标出每个 x 属于 LEGB 的哪一层,然后运行验证:
x = "A"
def outer():
x = "B"
def middle():
def inner():
print("1:", x)
inner()
x_local = x
print("2:", x_local)
middle()
print("3:", x)
outer()
print("4:", x)
def tricky():
print("5:", x)
x = "C"
tricky()
最后那个 tricky() 会报错。说明它报的是什么错,以及为什么。
14-6 重写成推导式
把下面三段函数式代码改写成推导式或内置函数,比较可读性:
# A
from functools import reduce
total = reduce(lambda a, b: a + b, map(lambda x: x ** 2, filter(lambda x: x % 2, nums)))
# B
names = list(map(lambda p: p["name"].upper(), filter(lambda p: p["age"] >= 18, people)))
# C
longest = reduce(lambda a, b: a if len(a) >= len(b) else b, words)
团队项目:简化版 NumPy
占总成绩 25% — 本学期分值最高的一项 分组:2 人 第 16 周提交 + 现场 Demo
背景
L10 提过 NumPy 是 Python 科学计算的基石。它的核心是一个叫 ndarray 的类型——多维数组,支持整体运算而不用写循环。
这个项目让你实现它的一个子集。你会用上这门课几乎所有的内容:类与运算符重载(L9、L10)、复杂度(L11)、异常(L11)、生成器(L13)、测试(L11)。
必做部分(70%)
实现一个 Array 类,支持一维和二维:
class Array:
def __init__(self, data: list) -> None:
"""从嵌套列表构造。要校验形状是否规整。"""
# 基本属性
@property
def shape(self) -> tuple[int, ...]: ...
@property
def ndim(self) -> int: ...
@property
def size(self) -> int: ...
# 表示
def __repr__(self) -> str: ...
def __eq__(self, other: object) -> bool: ...
# 索引
def __getitem__(self, key): ... # 支持 a[0]、a[1, 2]、a[0:2]
def __setitem__(self, key, value): ...
# 逐元素运算
def __add__(self, other): ... # Array + Array,或 Array + 标量
def __sub__(self, other): ...
def __mul__(self, other): ... # 逐元素乘,不是矩阵乘
def __truediv__(self, other): ...
def __neg__(self): ...
# 归约
def sum(self, axis: int | None = None): ...
def mean(self, axis: int | None = None): ...
def max(self, axis: int | None = None): ...
def min(self, axis: int | None = None): ...
# 形状
def reshape(self, *shape: int) -> "Array": ...
def transpose(self) -> "Array": ...
外加几个模块级函数:
def zeros(*shape: int) -> Array: ...
def ones(*shape: int) -> Array: ...
def arange(n: int) -> Array: ...
def dot(a: Array, b: Array) -> Array: ... # 矩阵乘法
选做部分(各 +5%,最多 +30%)
- 广播(broadcasting):形状
(3,1)和(1,4)的数组能相加,结果是(3,4) - 布尔索引:
a[a > 5]返回所有大于 5 的元素 - 更多维度:支持三维及以上
- 性能优化:用
array模块或扁平列表 + 步长(stride)代替嵌套列表,并给出实测对比 __format__与漂亮打印:像 NumPy 那样对齐输出
分项
| 部分 | 分值 |
|---|---|
| 必做功能正确 | 40% |
| 测试 | 15% |
| 代码质量与设计 | 10% |
| 文档(README) | 5% |
| 技术报告 | 10% |
| 选做加分 | 最多 +30%(总分不超过 100%) |
| 合计 | 80% + 加分 |
分工要求
两人必须都写代码。 提交时要包含一份分工说明,写明每个人负责了哪些部分。
项目不答辩,只提交,但署名是两个人的:交上去的每一部分,两个人都要能解释—— “那部分是我队友写的,我不清楚”不是说得过去的话。
本周要交的(不计分,但必须交)
- 组队名单:两人的姓名学号
- 一页设计草案:你们打算怎么存数据(嵌套列表?扁平列表 + 形状?)、怎么分工、打算做哪些选做部分
提交前自查
- 14-1 先预测后运行,标出了猜错的
- 14-2、14-3、14-4 都用了
functools.wraps - 14-4 回答了「对什么函数不安全」
- 14-5 标出了每个 x 的层级,解释了报错
- 14-6 三段都改写了
- 团队项目:组队名单已交
- 团队项目:一页设计草案已交
- AI 使用声明写了