CS1602计算导论
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% + 加分

分工要求

两人必须都写代码。 提交时要包含一份分工说明,写明每个人负责了哪些部分。

项目不答辩,只提交,但署名是两个人的:交上去的每一部分,两个人都要能解释—— “那部分是我队友写的,我不清楚”不是说得过去的话。

本周要交的(不计分,但必须交)

  1. 组队名单:两人的姓名学号
  2. 一页设计草案:你们打算怎么存数据(嵌套列表?扁平列表 + 形状?)、怎么分工、打算做哪些选做部分

提交前自查

  • 14-1 先预测后运行,标出了猜错的
  • 14-2、14-3、14-4 都用了 functools.wraps
  • 14-4 回答了「对什么函数不安全」
  • 14-5 标出了每个 x 的层级,解释了报错
  • 14-6 三段都改写了
  • 团队项目:组队名单已交
  • 团队项目:一页设计草案已交
  • AI 使用声明写了