CS1602计算导论
第 8 讲Part 3 问题求解与 AI 协作AI Level 1

AI 辅助编程(一):需求、审查与验证

AI-Assisted Programming I: Specification, Review and Verification

一份程序通过样例,是否就能采纳?本讲从函数规格出发,结合代码阅读、文档、测试与调试,检查功能、边界、输入变化和效率,并记录 AI 的参与范围。

本讲结束后你应当能

  • 区分代码生成、工具执行与正确性验证
  • 用函数签名、行为约定和边界例子说明需求
  • 按 PEP 8 和函数任务审查可读性,不把风格检查当作正确性证明
  • 设计测试、缩小反例,使用 print 或调试器定位问题
  • 检查重复计算与数据规模,记录采纳代码的依据
  • 据实填写 AI 使用声明,区分本周实验与个人作业 1 的要求
本页目录

一、候选代码与验证依据

L07 检查了递归的任务、基准情形、终止条件和返回值。相同的方法也适用于同学、网络资料或 AI 提供的代码。

一个程序通过了样例,说明它在这组输入上得到了预期结果。输入范围、边界情况、状态变化和数据规模,仍需要分别检查。解释流畅、代码简短,都不能补充尚未进行的验证。

下面的错误程序是为教学构造的候选实现,不是真实模型回答的逐字记录。它们用于说明检查方法,不代表某个模型必然产生这些错误。

生成文本与执行代码

这里的 token 是模型处理文本的单位,可能对应一个词、词的一部分或标点,并不固定等于一个字符。大语言模型根据上下文生成后续 token。生成代码和解释,并不自动产生执行结果。

带工具的助手还可以查询文档、运行程序或执行测试。需要区分三种记录:

记录能说明什么不能由此推出什么
一段候选代码及其解释提供了某种实现和理由程序已经运行,或实现符合全部需求
一次运行的输入和输出这次运行的实际行为所有输入都正确
一组测试及其结果这些检查是否通过测试覆盖了所有缺陷

采纳实现时,还要把这些证据与需求、代码逻辑和运行环境一起核对。AI 可以帮助分析和测试;提交者仍须理解采用的实现。

三类常见检查

接口是否存在。 字符串没有 reverse() 方法:

s = "hello"
print(s.reverse())

运行会触发 AttributeError。应查阅对应对象的文档,确认方法名称、参数、返回值和副作用。不能因为列表有 reverse(),就假定字符串也有同名方法。

下面的切片产生反转后的新字符串,原字符串不变:

s = "hello"
print(s[::-1])
print(s)

行为与边界是否明确。 这段平均值程序对非空列表正常,对空列表会除以零:

def average(nums: list[float]) -> float:
    return sum(nums) / len(nums)

print(average([1, 2, 3]))
print(average([]))

若规格只接受非空输入,空列表不在输入范围内。若任务需要接受空列表,就必须另外约定其行为。不能先看实现怎么处理,再把实现的行为当作需求。

运行环境是否一致。 旧语法、库版本和接口签名需要核对。下面是 Python 2 的写法,在本课程 Python 3.13 中不能运行:

# Python 2 syntax, not valid in the course's Python 3 environment.
print "hello"

这三类检查之外,还要审查效率和可读性。程序不报错,不等于它适合任务的数据规模,也不等于它易于维护。

二、函数规格与提问

签名说明接口,行为约定说明任务

“合并两个列表,排好序”还有很多未决定的细节:两个输入本来是否有序,重复项是否保留,是否可以修改输入,返回什么类型。

函数签名可以说明参数和返回类型:

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Merge two ascending integer lists into a new ascending list.
    Keep duplicates. Do not modify either input.
    """
    ...

排序、保留重复项和输入不变来自说明文字,不能从 list[int] 推出。这个片段只是接口规格,... 不是已经完成的实现。

Python 不会自动按照类型提示检查实参。下面的调用仍执行字符串拼接:

def twice(x: int) -> int:
    return x + x

print(twice("ab"))

类型检查器和编辑器可以利用注解提示问题,但注解本身不会自动保证行为。可执行的检查还需要测试或其他专门机制。

状态变化也是需求

预测下面两次打印的结果:

def without_two(items):
    result = items
    result.remove(2)
    return result

values = [1, 2, 3, 2]
print(without_two(values))
print(values)

两个名字指向同一个列表;remove(2) 只删除首个匹配项。因此两行都是 [1, 3, 2]。

若任务要求“删除全部匹配项,返回新列表,不修改输入”,可以用已经学过的列表推导:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
print(remove_all(values, 2))
print(values)

检查输入是否改变,可以在调用前保存副本:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
before = values.copy()
result = remove_all(values, 2)
print(result == [1, 3])
print(values == before)

上下文、约束和例子

一份便于检查的提问至少应包含:

  • 函数任务、签名和输入范围;
  • 返回值、顺序、重复项及输入是否改变;
  • 使用的 Python 版本、允许的库和已掌握的语法;
  • 普通与边界输入的预期结果;
  • 数据规模和需要满足的效率要求。

例如,词频任务可写为:

Implement count_words(text: str) -> dict[str, int] in Python 3.13.
Split on whitespace and ignore case.
Strip only . , ! ? ; : from the two ends of each word.
Do not count a token that becomes empty after stripping.
Use only syntax covered in Lectures 1–7.

Examples:
"The cat. THE dog" -> {"the": 2, "cat": 1, "dog": 1}
"" -> {}
"... !" -> {}

这些约定仍要按任务检查,不能把一份模板用于所有程序。列出空输入有助于明确要求,但不能据此保证模型不再遗漏边界。

小任务与可复现反馈

L07 用较小子问题组织实现。协作时也可以先完成一个接口明确的函数,检查后再进入下一项任务。一次返回的代码超出当前能阅读和解释的范围时,应缩小任务,而不是直接提交。

反馈失败时,保留以下信息:

Function: max_gap
Input: [7]
Expected result: 0, as specified for fewer than two elements
Actual result: ValueError from max([])
Runtime: Python 3.13
Relevant code: the construction of gaps and the final max(gaps)

不要只写“错了,再改”。AI 对原因的解释也是待核对的候选分析,不能替代反例和运行记录。

三、可读性与 PEP 8

先读出任务,再改写结构

下面的程序能够运行,但函数名、参数名和嵌套结构增加了阅读负担:

def f(l, D):
    R = []
    for i in range(0, len(l)):
        if l[i] in D:
            if D[l[i]] > 0:
                R.append(l[i])
    return R

print(f(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))

先说清行为:按输入顺序保留字典中计数为正的键。输入键有重复时,重复项也保留。再改名、整理条件:

def filter_positive(
    keys: list[str], counts: dict[str, int]
) -> list[str]:
    """Keep present keys with positive counts, in input order."""
    return [k for k in keys if counts.get(k, 0) > 0]

print(filter_positive(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))
print(filter_positive(["a", "x", "a"], {"a": 2}))

没有类型提示或文档字符串,不直接等于违反 PEP 8;本例补上它们,是为了明确接口和任务。重构还需要核对改写前后的行为。

常见风格检查

PEP 8 是 Python 代码风格指南。它强调可读性与一致性,不证明程序的功能正确。

项目常见约定
缩进每级 4 个空格;不要混用 Tab 和空格
行长通常代码 79 字符、注释和文档字符串 72 字符;团队可约定其他长度
空行顶层函数和类定义之间两空行;类内方法之间一空行
函数、变量snake_case,如 student_count
常量UPPER_CASE,如 MAX_RETRY
类CapWords,如 BankAccount;具体语法在 L09 学习
模块、包通常为简短小写名称,如 utils、mypackage
内部使用前导下划线,如 _helper;不是权限检查
易混淆名字避免单字母 l、O、I
内置名避免用 list、str、sum、type 等覆盖需要使用的内置接口

续行可在括号内对齐,或使用悬挂缩进:

result = some_function(first_argument,
                       second_argument)

result = some_function(
    first_argument,
    second_argument,
)

这是排版示例,不是完整可运行程序。长表达式在运算符之前或之后换行都可以,局部应保持一致;PEP 8 建议新代码在运算符之前换行:

income = (gross_wages
          + taxable_interest
          - ira_deduction)

括号内侧不加多余空格;逗号、冒号前不加空格;赋值和比较运算符两侧留空格。运算优先级不同的表达式,可以依指南适当调整空格来体现分组。风格差异不改变运算符优先级,也不自动构成语法错误。

# Recommended layout.
spam(ham[1], {eggs: 2})
if x == 4:
    print(x, y)

# Legal in suitable contexts, but not the recommended layout.
spam( ham[ 1 ], { eggs: 2 } )

无注解的默认参数和调用中的关键字实参,等号两侧不加空格;带注解的默认参数则加空格:

def f(x, y=2):
    ...


def g(x: int, y: int = 2) -> int:
    ...

格式化、静态检查与行为测试

三种工作不要混为一谈:

工作示例用途
格式化ruff format file.py,或使用 Black调整空格、换行等排版
静态检查ruff check file.py按启用的规则发现未使用导入、未定义名字等问题
行为测试运行自己的输入并比较预期结果检查具体需求

Ruff 的配置决定启用哪些规则。默认检查不是“全部 PEP 8”;没有报告,也不表示没有逻辑错误。ruff format 不会自动决定空列表求平均应该返回什么。

python3 -m pip install ruff
ruff check file.py
ruff format --check file.py     # check without rewriting

在 VS Code 中也可以使用 Ruff 扩展。若 pip 提示环境由系统管理,不要改用管理员权限强制安装;选择课程安装的 Python,或使用编辑器扩展。虚拟环境的系统介绍在 L10。Black 是另一种格式化选择,不要求与 Ruff 同时安装。

四、测试、反例与调试

测试预期来自规格

为 max_gap 约定:输入为整数列表,返回排序后相邻元素的最大差;少于两个元素时返回 0;不修改输入。

输入预期检查内容
[3, 9, 1, 15]6普通未排序输入
[]0空输入
[7]0单元素
[2, 2, 2]0重复项
[-3, -1, -10]7负数

候选程序遗漏短输入:

def max_gap(nums: list[int]) -> int:
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

print(max_gap([7]))

[7] 使 gaps 为空,max([]) 触发 ValueError。修正后,先检查已知反例,再检查原本成功的输入:

def max_gap(nums: list[int]) -> int:
    if len(nums) < 2:
        return 0
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

for nums, expected in [([3, 9, 1, 15], 6), ([7], 0), ([], 0)]:
    before = nums.copy()
    got = max_gap(nums)
    print(got == expected, nums == before)

这是回归检查。有限测试可以暴露错误,通常不能证明函数对所有输入正确。还要阅读循环范围、边界判断和输入变化。

常用检查包括空输入、一个元素、重复项、零与负数、数据规模、结果顺序和副作用。错误类型是否需要处理,取决于接口约定;一个只接受整数列表的函数,不必未经要求就接受任意对象。

阅读回溯

异常常见含义或例子
SyntaxError语法不合法,如缺少冒号
IndentationError缩进层级不合法
NameError使用尚未定义的名字
TypeError运算或调用不接受当前类型/参数形式
ValueError类型可接受,但值不满足接口要求,如 int("abc")
IndexError序列下标超出范围
KeyError字典中没有被直接索引的键
ZeroDivisionError除数为零
AttributeError对象没有访问的属性或方法
RecursionError当前执行超过解释器的递归深度限制

回溯包含调用位置与失败语句,最后一行通常给出异常类型和信息。现代 Python 还可能显示源码、波浪线和箭头;不能固定说“倒数第二行就是错误行”。递归深度异常既可能来自错误递推,也可能来自合法但过深的输入。L11 再讲异常处理语法。

从反例观察中间状态

下面的候选返回最长连续 1 的长度,输入只含 0 和 1,空列表应返回 0:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return best

print(longest_run([1, 1, 0]))
print(longest_run([1, 1, 0, 1, 1, 1]))

第一例得到 2,第二例也得到 2,但预期为 3。返回前 best=2、current=3:末尾连续段没有遇到 0,未更新 best。

可以暂时打印这两个变量,也可以在 VS Code 的返回语句前设断点:

  1. 启用 Python 与 Python Debugger 扩展,选择课程解释器;
  2. 点击语句左侧设置断点,通过“运行和调试”启动;
  3. 执行到该位置时,程序在语句执行前暂停;
  4. 查看当前选中调用帧的变量,或把 best、current 加入 Watch。
动作常见快捷键作用
ContinueF5继续到下一断点或结束
Step OverF10执行当前行,不进入被调用函数
Step IntoF11进入当前行调用的函数
Step OutShift+F11执行完当前函数,回到调用者

系统、键盘或自定义设置可能改变快捷键,部分键盘需同时按 Fn;工具栏动作名称也可使用。Watch 在暂停时按当前上下文求值,不是程序运行期间的连续监视器。

按本题约定,把最后一段与已记录最大值一起比较即可:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return max(best, current)

for bits in [[], [0], [1], [1, 1, 0], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

递归调用帧

L07 的 factorial(3) 进入 factorial(1) 时,调用栈中有参数为 1、2、3 的三个调用。选中不同帧,看到的是各自局部的 n。内层 n=1 不会把外层的 n=3 改成 1。

print 适合少量中间值和批量记录;调试器适合暂停、单步和切换调用帧。两者均可使用,不以一种工具替代全部调试方法。

五、效率与数据规模

这个去重实现保留首次出现顺序:

def dedup(items: list[int]) -> list[int]:
    result = []
    for x in items:
        if x not in result:
            result.append(x)
    return result

print(dedup([3, 1, 3, 5, 1]))

阅读代码就能注意到:每个输入元素都会在逐渐变长的列表中查找。可以进一步把查找写成显式循环,统计检查了多少个已有元素:

def dedup_counted(items: list[int]) -> tuple[list[int], int]:
    result = []
    comparisons = 0
    for x in items:
        found = False
        for old in result:
            comparisons += 1
            if old == x:
                found = True
                break
        if not found:
            result.append(x)
    return result, comparisons

for n in [10, 100, 1000]:
    result, checks = dedup_counted(list(range(n)) * 2)
    print(n, checks)

这组特定输入中,n 个不同元素依次出现两轮,显式比较数为 n²。这里没有测 CPython 的机器指令数,也没有证明所有数据分布都恰好需要 n² 次检查。

L06 的集合可保存已经见过的整数,列表继续保存结果顺序:

def dedup(items: list[int]) -> list[int]:
    seen = set()
    result = []
    for x in items:
        if x not in seen:
            seen.add(x)
            result.append(x)
    return result

for items in [[], [3, 1, 3, 5, 1], [-1, 0, -1]]:
    before = items.copy()
    print(dedup(items), items == before)

集合查找具有较低的平均代价,不保证每次查找固定一步。本例输入为可哈希整数;不能未经检查就用于列表元素等不可哈希对象。

比较速度时应使用相同输入、环境和测量方法,并核对结果等价。秒数受硬件、解释器和其他任务影响,不能把一次计时的倍率当作通用结论。L11 再系统分析复杂度、时间和空间。

六、审查活动与使用声明

四份候选实现

本讲已经检查了四类候选:

  1. 不存在的 items.remove_all(value):用文档和执行确认接口;
  2. max_gap:用短输入暴露缺失边界;
  3. dedup:分析反复列表查找,再核对操作数和规模;
  4. f(l,D):读出行为,改名和整理结构,检查等价性。

不要把每份候选都预设为“恰好一处错误”。也不要为了报告数量,把输入约定之外的行为自动认定为缺陷。

IPv4 任务的检查约定

Lab08 使用 is_valid_ipv4(s: str) -> bool。本活动的输入为字符串,并采用以下严格点分十进制约定:

  • 恰好四段,以英文句点 . 分隔;
  • 每段只含 ASCII 数字 0 到 9,数值为 0–255;
  • 单个 0 合法,其他形式不允许前导零;
  • 不接受空段、正负号、前后或内部空白。

先独立写测试,再查看候选。合法例子可以包括 0.0.0.0、192.168.1.1、255.255.255.255;非法例子要覆盖段数、范围、前导零和字符范围。这里不提供实验的完整正确实现。

文档和实跑可以帮助发现:int() 接受的字符串范围比这份规格更宽;isdigit() 也不只接受 ASCII 数字。

for s in ["01", "+1", " 1", "1"]:
    print(repr(s), int(s))

for s in ["1", "1", "²"]:
    print(repr(s), s.isdigit())

ASCII 限定是本活动明确规定的要求;Python ipaddress 文档说明四段和前导零规则,但该页面没有逐字给出这条字符范围限定。检查陌生接口时要区分文档原句、实测行为与本题约定。

分工与安全

环节应保留的工作
理解和拆解明确目标,决定输入、结果和接口
实现可以由本人编写,或按当前政策借助工具
审查读懂关键状态、未知接口和副作用
测试自己依据规格设计检查,工具可以补充
修正核对反例,保留原候选与修改范围
采纳能说明依据,并对提交内容负责

先明确需求,再逐步实现和验证。不要使用无法解释的写法;先查文档和运行环境。先实现功能,再根据数据规模决定是否需要优化,不以“不要提前优化”为由忽略已知规模问题。

向外部工具提供材料前,移除个人信息、会话令牌、密码和密钥。未经审查,不运行包含不理解的文件操作、安装或网络请求的代码。错误示例只在自己可控制的小规模环境中运行。

AI 使用声明

按现有课程政策,本周实验可使用 AI,但须说明工具、生成范围和验证方式。以下是填写格式示例,不是真实使用日志:

AI 使用声明:
工具与模型:填写实际使用的名称;型号不清楚则注明。
生成范围:例如,remove_all() 的初始实现。
人工修改:指出修改的函数、条件或代码段;没有修改也据实说明。
验证:列出实际运行的输入、预期和结果,并说明是否检查输入不变。
未使用 AI 的部分:说明自行完成的范围。

“AI 说程序正确”不是运行记录。工具不可用而使用教师提供的候选时,也要据实填写来源,不能编造聊天记录。完全未使用 AI,就明确写未使用。

个人作业 1 的普通与相邻杆汉诺塔仍按 Level 0 执行,占总成绩 5%,截止见个人作业 1 提交页。本周进入 Level 1 不改变该作业要求。不能把个人作业代码交给 AI 生成或改写;本讲演示使用课堂阶乘等独立例子。

七、往年练习与参考规范

导入、注释和其他排版

保留以下查阅项目:

  • import 放在模块文档字符串之后,按标准库、第三方、本项目分组,组间空行;尽量避免通配导入覆盖不明名字。
  • 优先使用括号内隐式续行;避免行末空白、多条语句挤在同一行。
  • 单元素元组需要逗号;是否保留多行结构尾逗号按指南与项目约定。
  • 注释说明原因,更新代码时同步更新注释;文档字符串说明用途、参数和结果。
  • Python 3 默认源码编码为 UTF-8;引号选择保持一致,不把一种引号误称为唯一正确形式。
  • 类、继承、公共接口、异常命名等参考项,随后续讲次再展开,不要求现在掌握其语法。

导入分组示例仅展示风格,第三方与本项目模块不作为已安装依赖:

import os
import sys

import numpy as np

from mypackage import helper

完整条目见 PEP 8。Python 之禅可在本机或网页运行:

import this

它由 Tim Peters 撰写,强调可读性、明确性等设计取舍,不是逐行判分的正确性规则。

闰年:等价写法与边界

公历正整数年份,能被 4 整除且不能被 100 整除,或能被 400 整除,为闰年。以下三种结构对应相同规则:

def leap_separate(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    if year % 400 == 0:
        return True
    return False


def leap_branches(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    elif year % 400 == 0:
        return True
    else:
        return False


def leap_boolean(year: int) -> bool:
    return (year % 4 == 0 and year % 100 != 0
            or year % 400 == 0)

for year in [1900, 2000, 2024, 2100]:
    print(year, leap_separate(year), leap_branches(year), leap_boolean(year))

素数:输入范围与试除界限

素数是大于 1、只有 1 与自身两个正因数的整数。往年两个版本都遗漏 n<2;不能用几个正整数样例掩盖这个问题。

def prime_slow(n: int) -> bool:
    if n < 2:
        return False
    for divisor in range(2, n):
        if n % divisor == 0:
            return False
    return True


def prime_fast(n: int) -> bool:
    if n < 2:
        return False
    divisor = 2
    while divisor * divisor <= n:
        if n % divisor == 0:
            return False
        divisor += 1
    return True

for n in [-1, 0, 1, 2, 3, 4, 9, 97]:
    print(n, prime_slow(n), prime_fast(n))

若合数 n 有因数 d 大于平方根,配对因数 n//d 就小于平方根,所以不必试到 n−1。平方边界要包含等号。这里用整数乘法,避免以浮点数近似决定界限。

数位平方和与快乐数

非负整数的每位平方求和,可以用循环、递归或字符串遍历:

def squares_loop(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def squares_recursive(n: int) -> int:
    if n < 10:
        return n * n
    return (n % 10) ** 2 + squares_recursive(n // 10)


def squares_text(n: int) -> int:
    total = 0
    for ch in str(n):
        total += int(ch) ** 2
    return total

for n in [0, 2, 9, 19, 29]:
    print(n, squares_loop(n), squares_recursive(n), squares_text(n))

旧递归基准返回 n,对 2 得到 2 而不是 4;19 的样例恰好没有暴露这项错误。三种方法都需核对最小输入,递归还受深度限制。

对正整数反复应用数位平方和,若最终到 1,称为快乐数;若出现已访问值,后续将重复同一过程。

def sum_digit_squares(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def is_happy(n: int) -> bool:
    seen = set()
    while n != 1:
        if n in seen:
            return False
        seen.add(n)
        n = sum_digit_squares(n)
    return True

print(is_happy(19))
print(is_happy(2))

已访问集合表示经过的状态,不表示状态的次数;这里按任务只需判断是否重复。输入 0 不属于本定义的正整数范围。

连续的 1 与连续递增段

往年双 while 写法也能计算最长连续 1。虽然有嵌套循环,外层下标会越过已经扫描的连续段,不能只看外观就断言是平方工作量。

def longest_run(bits: list[int]) -> int:
    best = 0
    i = 0
    while i < len(bits):
        if bits[i] == 1:
            j = i
            while j < len(bits) and bits[j] == 1:
                j += 1
            best = max(best, j - i)
            i = j
        else:
            i += 1
    return best

for bits in [[], [0], [1], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

连续严格递增段必须使用相邻元素,相等时结束当前段;不能跳过中间元素拼成任意递增子序列:

def longest_increasing_run(nums: list[int]) -> int:
    if not nums:
        return 0
    current = 1
    best = 1
    for i in range(1, len(nums)):
        if nums[i] > nums[i - 1]:
            current += 1
        else:
            current = 1
        best = max(best, current)
    return best

for nums in [[], [1, 3, 5, 4, 7], [2, 2, 2], [1]]:
    print(nums, longest_increasing_run(nums))

字母重排与重数

本例输入为小写 ASCII 字符串。两串每个字符的出现次数相同,才互为字母重排。字典计数与排序均可实现:

def char_counts(s: str) -> dict[str, int]:
    counts = {}
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    return counts


def anagram_counts(s: str, t: str) -> bool:
    return char_counts(s) == char_counts(t)


def anagram_sorted(s: str, t: str) -> bool:
    return sorted(s) == sorted(t)

for s, t in [("anagram", "nagaram"), ("rat", "car"), ("aab", "abb"), ("", "")]:
    print(s, t, anagram_counts(s, t), anagram_sorted(s, t))

set("aab") 和 set("abb") 相同,却丢失了次数,不能据此判断字母重排。

布尔条件、简化与开发习惯

当参数确实是 bool 时,可以直接用 if flag。一般对象的真假值与 flag == True 并不总等价。L05 也讲过:列表上的 += 和 + 可能有不同的输入变化,不能为了缩短代码全部互换。

保留往年建议中的实际检查:明确任务与中间状态,按接口拆函数,完成小功能后测试,复用已有计算,用有含义的变量记录常量,查陌生接口,保留失败输入。函数长度、循环层数或代码简短本身都不保证正确;append() 也有合适的使用场景。

小结

采纳一份实现,需要明确需求、阅读关键逻辑、核对文档和环境、检查边界与副作用,并根据规模评估效率。工具执行和有限测试提供证据,但不能代替其余审查。

格式化工具整理排版,静态检查报告规则问题,print 与调试器帮助定位状态,测试检查具体约定。记录来源、反例、修改和回归结果,才能说明为什么采纳。

下一讲将讨论:同类记录的数据与操作,怎样用类一起组织。

练习

  1. 找出下列候选在非空和空输入上的失败,并说明缺少的规格。修订约定:空列表返回空字符串;频次并列时返回最先出现的词。自行修正并测试。
    def most_common(words: list[str]) -> str:
        counts = {}
        for word in words:
            counts[word] += 1
        return max(counts, key=counts.get)
  2. 为“把成绩列表转换为等级列表”写签名、行为说明及至少三个例子。明确分数范围、等级边界、空输入和输入是否改变。
  3. 按本讲 IPv4 规格,先独立写至少六个测试,再检查 AI 或教师提供的候选。记录预期、实际结果与修正。
  4. 用课堂 factorial() 检查递归与循环版本的等价性,包括 n=0。可以按本周政策请 AI 提供候选,但不得让它改写个人作业 1。
  5. 在自己 Labs 1–7 的代码上运行 Ruff。解释三项不熟悉的诊断;若不足三项,使用 Lab08 的练习文件,不编造警告。
  6. 阅读 PEP 8 的 “A Foolish Consistency is the Hobgoblin of Little Minds”,用一句话说明项目一致性、可读性与机械套规则的关系。

配套实验见 Lab08,已按 A/B/C 组织并准备本地题包与文字卡,尚未同步服务器。正式发布后使用 HW3 第八周入口,当前保存与提交方式以实验页为准。课程政策见 AI 使用政策。