qinghuan
← 从零建立基础 / 全部笔记

单元 03 / 从零建立基础

P0 · 必会主线L0 · 零基础

用列表、字典和函数处理第一份学习数据

用清洗标题的小任务学习列表、字典、函数和测试,掌握 AI 应用马上会用到的 Python。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能实现标题清洗与去重函数,并正确处理空列表、缺字段、空白和错误类型。

学习理由、准备要求与知识点

为什么学它

模型接入前后都要清洗和检查数据。把这些普通函数写对,才能判断问题出在数据处理还是模型回答。

先读这些内容

知识点清单

  • 字符串与空白清理
  • 列表索引与遍历
  • 字典取值及缺字段
  • 集合去重
  • 函数参数和返回值
  • 可变对象与副作用
  • 语法错误和运行异常
  • 正常与边界输入验证

运行前先确认完整算法使用 Java 17 标准库;JDK 源码基线为 OpenJDK 17 GA。示例是内存中的机制模型,外部服务接入需要另外配置和验证。

01先弄懂它是什么

接入模型前,可以先练习一个确定的数据处理任务:把标题列表“ Java ”、“SQL”、“Java”整理成“Java”、“SQL”。这里需要去掉首尾空白、过滤空标题、去重,并保留第一次出现的顺序。把这些规则写成普通函数后,就能直接检查结果,不必依赖模型是否在线。

选 Python 容器时,看自己要保留什么信息。字符串放文本,列表按顺序放多项数据,字典根据键查找值,集合方便检查一个值是否出现过。例如只用集合,重复次数就不会保留,也不应依赖它的遍历顺序;从字典读取字段前,则要先决定缺失时用默认值、允许为空,还是直接报错。

函数把输入变成输出,也可能改变外部对象。Python列表可变,Java集合也常可变;把输入直接删除或排序会影响仍持有它的调用者。入门阶段优先编写不修改输入的函数,并用正常、空、重复及错误输入构造测试,逐步理解语法错误、运行异常和业务拒绝之间的区别。

文中这几个词,先认一认

稳定去重
重复的值只留下第一次,并保留这些首次出现值之间原来的顺序。
副作用
函数除了返回结果,还改变了外部东西,例如传入的列表、文件或全局计数。
测试断言
把实际结果与事先写明的预期比较,不符合就让测试失败。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

清洗顺序会改变含义。必须先strip,再判断是否为空,再按清洗后的标题去重,否则“Java”和“ Java ”会被当成两条。这里大小写保留,Java与java不合并,因为课程标题是否忽略大小写属于业务规则。不要擅自增加清洗规则,导致原本不同的标题变成同一个值。

扫描列表时,用集合记录已出现的标题,用列表保存结果。遇到新标题加入两者,遇到重复标题跳过。Python可用seen集合配合result列表;Java可用插入顺序集合实现同样规则。两种写法都应让每个标题只出现一次,并按第一次出现的顺序返回;语言不同不应改变空值处理规则。

例如处理“ SQL ”后,结果为SQL;再遇到空白字符串则跳过;遇到Java追加;最后遇到SQL不动。若输入含null,本题明确拒绝,并指出那是类型契约失败。返回一个新列表确保调用者输入没有被修改,测试应同时核对输出和原输入。

先跑通一个最小例子

把 clean_titles.py 以 UTF-8 保存到独立项目目录,在该目录的 PowerShell 中逐行执行命令块。脚本使用 Python 3.12 兼容语法和标准库,无第三方依赖;py -3.12 要求本机已安装对应解释器,创建环境成功后再运行文件。

函数只接受字符串列表,先按 str.strip 清理首尾空白,再过滤空值并稳定去重,大小写不合并。自测覆盖七组正常边界和五组非法输入,每个正常用例同时检查输入未被修改、返回的是新列表;这些检查使用显式异常,不会因 Python 的 -O 选项被移除。

展开收起代码 · clean_titles.pypython · 49 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

clean_titles.py · python
def clean_titles(titles: list[str]) -> list[str]:
    if not isinstance(titles, list):
        raise TypeError("titles must be a list")  # 类型标注不会自动检查运行时输入。
    seen: set[str] = set()
    result: list[str] = []
    for index, raw in enumerate(titles):
        if not isinstance(raw, str):
            raise TypeError(f"titles[{index}] must be a str")
        title = raw.strip()  # 先清理首尾空白,使用 Python str.strip 的规则。
        if not title or title in seen:
            continue  # 空标题及清洗后已出现的标题都跳过。
        seen.add(title)
        result.append(title)  # 顺序来自列表追加,不能依赖 set 的迭代顺序。
    return result  # 返回新列表,不修改调用者的输入。


def run_tests() -> None:
    cases = [
        ([], []),
        ([" Java ", "SQL", "Java"], ["Java", "SQL"]),
        ([" ", "\t", "\n"], []),
        (["SQL", " SQL ", "SQL"], ["SQL"]),
        (["Java", "java"], ["Java", "java"]),
        (["B", "A", "B", "C", "A"], ["B", "A", "C"]),
        (["\u2003中文\u2003"], ["中文"]),
    ]
    for values, expected in cases:
        before = values.copy()
        actual = clean_titles(values)
        if actual != expected:
            raise AssertionError(f"{actual!r} != {expected!r}")
        if values != before or actual is values:
            raise AssertionError("input changed or output aliases input")
        # 每个正常用例同时检查值、输入未变和返回独立容器。

    invalid_inputs = [None, "Java", ("Java",), [None], [123]]
    for invalid in invalid_inputs:
        try:
            clean_titles(invalid)
        except TypeError:
            pass  # 非列表或含非字符串元素,必须按契约拒绝。
        else:
            raise AssertionError(f"invalid input accepted: {invalid!r}")
    print(f"tests={len(cases) + len(invalid_inputs)} passed")


if __name__ == "__main__":
    print(f"result={clean_titles([' Java ', 'SQL', 'Java', '', 'java'])}")
    run_tests()  # 任意检查失败都会抛异常,使进程以非零状态退出。
PowerShell 命令 · powershell
# 在保存 clean_titles.py 的独立项目目录打开 PowerShell。
# 前提:Python 3.12 已安装,且 py -3.12 能选中它。
py -3.12 --version
py -3.12 -m venv .venv
# 创建成功后运行纯标准库脚本,不需要安装第三方包。
.\.venv\Scripts\python.exe .\clean_titles.py

核对运行结果脚本应恰好输出两行: result=['Java', 'SQL', 'java'] tests=12 passed 并以退出码 0 结束。七组正常用例包括空列表、清理后重复、全空白、全重复、大小写保留、首次顺序和 Unicode 首尾空白;五组非法输入包括 None、单个字符串、元组、含 None 的列表及含整数的列表。任一输出、输入不变性或异常契约不符合预期,脚本都会以非零状态退出,不能只看到 result 行就认定测试通过。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart LR
  I["原始标题列表"] --> N["清理首尾空白"]
  N --> E{"是否为空"}
  E -->|"是"| K["跳过"]
  E -->|"否"| U["按首次顺序加入集合"]
  U --> R["复制为结果列表"]

按顺序看,每一步做了什么

  1. 列出清洗顺序、空值策略、大小写与重复规则。
  2. 为结果创建新的容器,避免修改调用者输入。
  3. 逐项清理空白,过滤空标题,再稳定去重。
  4. 返回结果并核对正常、空、全重复和非法输入。
  5. 把真实失败样本缩成最小回归用例。

03对照实现代码

OpenJDK 17 LinkedHashMap 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · LinkedHashMap.newNode / afterNodeInsertion

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
Map<String, Boolean> seen = new LinkedHashMap<>();
for (String raw : titles) {
    String title = raw.strip(); // 先规范化再比较,避免空白造成伪重复。
    if (!title.isEmpty()) seen.putIfAbsent(title, Boolean.TRUE);
}
List<String> result = new ArrayList<>(seen.keySet()); // 默认保持首次插入顺序。

LinkedHashMap在哈希映射之外维护连接顺序的节点关系,新节点接到链表尾部;默认构造使用插入顺序,因此可以保留第一次出现的标题位置。

afterNodeInsertion提供插入后的淘汰钩子,而是否使用访问顺序由构造配置决定。学习时要区分“默认插入顺序”与某些缓存使用的“访问顺序”,读取元素是否改变顺序取决于配置。

本题完整程序使用LinkedHashSet,它以有序映射支持集合语义;源码片段用LinkedHashMap显式呈现键和值。二者都没有替业务决定去空白、大小写或空值规则。

返回新ArrayList让调用者获得独立容器,但这只是浅复制;若元素是可变对象,内部对象仍可能共享。本例元素为不可变String,因此无需递归复制标题内容。

代码先调用谁,再调用谁

  1. 清洗原始字符串
  2. putIfAbsent检查是否已有键
  3. 新键追加到插入顺序链
  4. 按keySet顺序构造新结果

04动手写一个例子

稳定标题清洗

输入字符串列表,输出去除首尾空白、过滤空串并稳定去重的新列表;大小写敏感,null拒绝。Main.java使用Java17固定断言自测。

输入 1
[" Java ","SQL","Java",""]
输出 1
["Java","SQL"]
输入 2
[]
输出 2
[]

稳定性来自只在第一次见到清洗值时插入,而不是先排序后去重。排序会改变原始优先顺序,即使唯一元素集合相同,也违反题目契约。

把规范化结果作为集合键,确保比较的是最终展示标题。原始字符串保留在输入列表中,函数只保存新的字符串引用和容器,不执行remove或set。

遍历结束时把有序集合复制成列表,调用者可直接序列化输出。若业务需要重复次数,应换成有序计数映射而非悄悄在此函数返回额外语义。

为什么这样写是对的处理前i项后,有序集合恰好包含该前缀所有非空规范化标题,顺序为它们首次出现顺序,输入列表保持不变。

时间 平均O(C)额外空间 O(Cu+k)

C为输入总字符数,strip和哈希读取字符;Cu为保留下来的规范化字符串字符量,k为不同标题数量。哈希性能依赖合理分布,不能省略字符串处理成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static List<String> clean(List<String> titles) {
        if(titles==null)throw new IllegalArgumentException("null list");
        Set<String> seen=new LinkedHashSet<>();
        for(String raw:titles){
            if(raw==null)throw new IllegalArgumentException("null title");
            String title=raw.strip(); // 仅去首尾空白,保留内部空格及大小写。
            if(!title.isEmpty())seen.add(title); // 已存在时不改变首次插入位置。
        }
        return new ArrayList<>(seen); // 独立结果容器,不修改输入。
    }
    static void check(List<String> expected,List<String> actual){
        if(!expected.equals(actual))throw new AssertionError(actual);
    }
    public static void main(String[] args){
        List<String> input=new ArrayList<>(List.of(" Java ","SQL","Java"," "));
        check(List.of("Java","SQL"),clean(input));
        check(List.of(" Java ","SQL","Java"," "),input);// 检查无输入副作用。
        check(List.of(),clean(List.of()));
        check(List.of("A"),clean(List.of("A"," A ","A")));
        check(List.of("Java","java"),clean(List.of("Java","java")));
        try{clean(Arrays.asList("A",null));throw new AssertionError();}catch(IllegalArgumentException expected){}
        System.out.println("PASS 6 tests");
    }
}

这些特殊情况也要试一试

  • 全是空白时返回空列表,而不是保留一个空标题。
  • 大小写不同的标题仍是不同值,修改规则需同时改测试。
  • 浅复制不保护可变元素;此处String不可变才满足边界。
进阶选读 · 后续再读:大文件稳定去重

场景标题规模超过单机内存,需要去重且保留全局首次出现顺序。

做法给记录附原始序号,外排序按规范化键分组取最小序号,再按序号外排序恢复稳定顺序;把分块结果直接拼接会漏掉跨块重复。

复杂度比较排序外部工作量O(n log n),磁盘归并的I/O成本取决于内存与块大小;不能声称只需常量磁盘空间。

好处与代价精确稳定去重需要保存排序或已见信息;Bloom Filter节省内存但有误判,不能直接承担无误删要求。

05项目里怎么用

清洗函数应放在可单测的模块中,网络请求只负责得到原始数据。遇到字典缺字段时,例如title缺失,可以在转换DTO时给出明确错误位置;不要用空字符串默认值把缺字段与合法空标题混淆。错误记录可以被收集为报告,但是否跳过整条数据由调用方策略决定。

Python中不要把可变列表作为会在调用间共享的默认参数。需要默认空集合时可用None并在函数内部创建;Java中也要警惕共享静态集合。通过连续调用两次同一个函数,可以检查第一次结果是否污染第二次结果,这类状态泄漏比单次输出更容易被忽略。

模型输入预处理应保留原文和变换说明,便于追溯。去除代码缩进、合并空白或改变大小写可能改变语义,标题清洗规则不能直接套用到代码块。先测可复现的清洗,再评估模型输出,才能判断错误究竟来自资料变形还是生成过程。

容易踩的坑

  • 用普通集合输出并依赖偶然稳定的迭代次序。
  • 先去重后strip,让只差空白的标题重复出现。
  • 捕获所有异常后返回空列表,掩盖契约错误。
  • 原地修改输入,导致另一个调用者看到意外变化。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
有序集合实现短,稳定去重直接只保留出现与否标题列表和小型元数据
有序计数映射保留频次与首次顺序值与更新逻辑稍多需要报告重复次数
外排序内存有界且可精确处理大数据增加磁盘I/O和流水线复杂度超内存批量资料处理

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01集合去重为什么不自动等于稳定去重?

展开参考答案与推导

唯一性只要求元素不重复,稳定性还要求保留首次出现顺序;应选择有此契约的数据结构或单独维护结果列表。

02纯函数与异常冲突吗?

展开参考答案与推导

不冲突。对相同合法输入返回相同输出且不改外部状态,也可以对非法输入确定地拒绝;异常的处理方式由上层决定。

03复制列表为什么未必隔离所有修改?

展开参考答案与推导

浅复制只复制元素引用。元素若可变,两个列表仍可指向同一对象;需要不可变DTO或按业务需要复制内部字段。

换个情况,还会处理吗

01代码块清洗后编译失败,先查什么?

展开参考答案与推导

比较原文与处理后文本,重点查看缩进、换行及引号;标题strip规则不能直接作为代码片段规范化规则。

02第二次调用出现上次标题,如何定位?

展开参考答案与推导

检查共享全局容器、可变默认参数和缓存键;建立两次独立调用的断言,预期第二次只由自己的输入决定。

03数据有一个null就全部失败,是否应该吞掉?

展开参考答案与推导

先明确产品策略。可收集逐条错误并继续,但必须返回错误计数和位置;静默丢弃会让调用方误认为资料完整。

写代码试一试

01输入顺序反转后结果如何?

输入 / 条件
SQL,Java,SQL
输出 / 预期
SQL,Java

提示 只保留首次位置。

展开参考答案与推导

第一次SQL先进入有序集合,Java随后加入,最后SQL重复不移动位置。

02如何保留重复次数?

输入 / 条件
A,A,B
输出 / 预期
A:2,B:1

提示 使用有序映射。

展开参考答案与推导

先规范化,再对LinkedHashMap调用merge累计,迭代时保持首次插入顺序;空值规则与原函数一致。

03怎样证明函数没有修改输入?

输入 / 条件
可变输入[" A ","A"]
输出 / 预期
结果[A],输入仍[" A ","A"]

提示 同时断言两个对象的内容。

展开参考答案与推导

调用前复制输入快照,调用后比较原输入与快照;再修改结果容器,检查原列表仍不受影响。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能实现标题清洗与去重函数,并正确处理空列表、缺字段、空白和错误类型。

本篇重点加练

可变数据的副作用与边界测试决定清洗函数能否安全组合使用。

  • 可变对象与副作用
  • 正常与边界输入验证

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    设计处理笔记标题列表的函数契约,说明空白清理、原始顺序和索引越界的处理;给出包含空列表和全空白标题的输入。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点字符串与空白清理 · 列表索引与遍历

  2. 作业 02代码推演

    对包含缺字段、重复标签和空标签的三条字典记录手工推演清洗过程,说明去重单位与缺失数据规则,保留中间结构。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点字典取值及缺字段 · 集合去重

  3. 作业 03原理问答

    为“清洗后仍能显示原始输入”制定所有权合同,说明调用者、函数与返回结果分别允许修改哪些对象,以及如何验证合同。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点可变对象与副作用 · 正常与边界输入验证

综合应用

  1. 作业 04面试场景

    同一个列表传入两个函数后原数据意外变化。构造最小复现,说明参数、返回值和可变对象共享关系,并提出可检验的契约。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点函数参数和返回值 · 可变对象与副作用

  2. 作业 05编程实践

    编写笔记清洗程序,为语法错误、类型不符、缺字段和合法输入分别编排测试;说明哪些错误属于代码本身,哪些属于输入处理。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点语法错误和运行异常 · 正常与边界输入验证

  3. 作业 06排错实验

    对嵌套列表执行你自写的复制与修改逻辑,记录修改前后各对象内容和身份,检验复制是否符合预期契约。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点列表索引与遍历 · 字典取值及缺字段 · 可变对象与副作用

  4. 作业 07编程实践

    实现两步笔记处理流水线,第一步清洗、第二步生成统计。交换两步的执行顺序并加入共享输入,按你声明的合同记录允许与不允许的结果差异。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点可变对象与副作用 · 正常与边界输入验证

  5. 作业 08排错实验

    给函数连续传入同一可变对象,比较第一次与第二次执行的结果;构造能暴露意外累积状态的输入,并保留失败测试。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点可变对象与副作用 · 正常与边界输入验证

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    在学会文件读取后,把清洗函数接入逐行数据处理,记录坏记录的位置并继续处理其余记录,提交与一次性处理的结果对照。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点字符串与空白清理 · 集合去重 · 函数参数和返回值

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 反例构造

    审查“覆盖空列表与正常列表就足以证明函数没有副作用”。构造一个同时满足这些样例却违反所有权合同的实现或纸面反例。

    提交内容
    具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
    怎样算完成
    • 反例满足原主张所声称的前提,确实针对其结论。
    • 说明一个反例能推翻什么、不能进一步证明什么。

    练到的知识点可变对象与副作用 · 正常与边界输入验证

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗