读完这篇,你能做什么
能实现标题清洗与去重函数,并正确处理空列表、缺字段、空白和错误类型。
学习理由、准备要求与知识点
为什么学它
模型接入前后都要清洗和检查数据。把这些普通函数写对,才能判断问题出在数据处理还是模型回答。
先读这些内容
知识点清单
- 字符串与空白清理
- 列表索引与遍历
- 字典取值及缺字段
- 集合去重
- 函数参数和返回值
- 可变对象与副作用
- 语法错误和运行异常
- 正常与边界输入验证
运行前先确认完整算法使用 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.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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() # 任意检查失败都会抛异常,使进程以非零状态退出。
# 在保存 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["复制为结果列表"]按顺序看,每一步做了什么
- 列出清洗顺序、空值策略、大小写与重复规则。
- 为结果创建新的容器,避免修改调用者输入。
- 逐项清理空白,过滤空标题,再稳定去重。
- 返回结果并核对正常、空、全重复和非法输入。
- 把真实失败样本缩成最小回归用例。
03对照实现代码
OpenJDK 17 GA,jdk-17+35 · LinkedHashMap.newNode / afterNodeInsertion
下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。
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,因此无需递归复制标题内容。
代码先调用谁,再调用谁
- 清洗原始字符串
putIfAbsent检查是否已有键- 新键追加到插入顺序链
- 按
keySet顺序构造新结果
04动手写一个例子
稳定标题清洗
输入字符串列表,输出去除首尾空白、过滤空串并稳定去重的新列表;大小写敏感,null拒绝。Main.java使用Java17固定断言自测。
- 输入 1
[" Java ","SQL","Java",""]- 输出 1
["Java","SQL"]
- 输入 2
[]- 输出 2
[]
稳定性来自只在第一次见到清洗值时插入,而不是先排序后去重。排序会改变原始优先顺序,即使唯一元素集合相同,也违反题目契约。
把规范化结果作为集合键,确保比较的是最终展示标题。原始字符串保留在输入列表中,函数只保存新的字符串引用和容器,不执行remove或set。
遍历结束时把有序集合复制成列表,调用者可直接序列化输出。若业务需要重复次数,应换成有序计数映射而非悄悄在此函数返回额外语义。
为什么这样写是对的处理前i项后,有序集合恰好包含该前缀所有非空规范化标题,顺序为它们首次出现顺序,输入列表保持不变。
C为输入总字符数,strip和哈希读取字符;Cu为保留下来的规范化字符串字符量,k为不同标题数量。哈希性能依赖合理分布,不能省略字符串处理成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
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 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能实现标题清洗与去重函数,并正确处理空列表、缺字段、空白和错误类型。
本篇重点加练
可变数据的副作用与边界测试决定清洗函数能否安全组合使用。
- 可变对象与副作用
- 正常与边界输入验证
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
设计处理笔记标题列表的函数契约,说明空白清理、原始顺序和索引越界的处理;给出包含空列表和全空白标题的输入。
- 提交内容
- 一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
- 怎样算完成
- 逐项回应题目要求,区分事实、假设与个人判断。
- 能用自己的例子解释适用条件,记录尚未弄懂的点。
练到的知识点字符串与空白清理 · 列表索引与遍历
- 作业 02代码推演
对包含缺字段、重复标签和空标签的三条字典记录手工推演清洗过程,说明去重单位与缺失数据规则,保留中间结构。
- 提交内容
- 手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
- 怎样算完成
- 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
- 主动检查至少一个边界或异常条件,能解释手算与运行差异。
练到的知识点字典取值及缺字段 · 集合去重
- 作业 03原理问答
为“清洗后仍能显示原始输入”制定所有权合同,说明调用者、函数与返回结果分别允许修改哪些对象,以及如何验证合同。
- 提交内容
- 概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
- 怎样算完成
- 关键概念有明确边界,能用案例检验声明。
- 未获证据支持的假设单独标注。
练到的知识点可变对象与副作用 · 正常与边界输入验证
综合应用
- 作业 04面试场景
同一个列表传入两个函数后原数据意外变化。构造最小复现,说明参数、返回值和可变对象共享关系,并提出可检验的契约。
- 提交内容
- 场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
- 怎样算完成
- 方案回应给定约束,列出至少一种替代方案及代价。
- 每项关键结论说明所需证据,不能核实的部分明确标注。
练到的知识点函数参数和返回值 · 可变对象与副作用
- 作业 05编程实践
编写笔记清洗程序,为语法错误、类型不符、缺字段和合法输入分别编排测试;说明哪些错误属于代码本身,哪些属于输入处理。
- 提交内容
- 可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
- 怎样算完成
- 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
- 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。
练到的知识点语法错误和运行异常 · 正常与边界输入验证
- 作业 06排错实验
对嵌套列表执行你自写的复制与修改逻辑,记录修改前后各对象内容和身份,检验复制是否符合预期契约。
- 提交内容
- 最小故障输入、复现步骤、观测记录、定位依据及回归用例。
- 怎样算完成
- 其他人可按记录复现故障,并区分现象与已验证原因。
- 定位过程有观测依据,回归用例可以再次暴露原问题。
练到的知识点列表索引与遍历 · 字典取值及缺字段 · 可变对象与副作用
- 作业 07编程实践
实现两步笔记处理流水线,第一步清洗、第二步生成统计。交换两步的执行顺序并加入共享输入,按你声明的合同记录允许与不允许的结果差异。
- 提交内容
- 可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
- 怎样算完成
- 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
- 验证目标由合同确定,模拟结果不冒充真实服务证据。
练到的知识点可变对象与副作用 · 正常与边界输入验证
- 作业 08排错实验
给函数连续传入同一可变对象,比较第一次与第二次执行的结果;构造能暴露意外累积状态的输入,并保留失败测试。
- 提交内容
- 最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
- 怎样算完成
- 故障可以稳定重现,现象与已验证原因分开。
- 观测记录足以判断声明的合同是否被违反。
练到的知识点可变对象与副作用 · 正常与边界输入验证
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
在学会文件读取后,把清洗函数接入逐行数据处理,记录坏记录的位置并继续处理其余记录,提交与一次性处理的结果对照。
- 提交内容
- 设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
- 怎样算完成
- 比较使用一致条件与事先声明的标准,记录失败与未知结果。
- 说明方案的适用前提、未覆盖风险及后续验证方法。
练到的知识点字符串与空白清理 · 集合去重 · 函数参数和返回值
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
审查“覆盖空列表与正常列表就足以证明函数没有副作用”。构造一个同时满足这些样例却违反所有权合同的实现或纸面反例。
- 提交内容
- 具体反例、原主张的适用前提、复现或逻辑证明及结论边界。
- 怎样算完成
- 反例满足原主张所声称的前提,确实针对其结论。
- 说明一个反例能推翻什么、不能进一步证明什么。
练到的知识点可变对象与副作用 · 正常与边界输入验证
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程中文
Python 3.12:数据结构 (新标签页)↗
重点阅读列表、字典、集合和遍历方法,用少量笔记记录完成取字段、去重与清洗。
- 实践教程中文
Python 3.12:控制流和函数 (新标签页)↗
先练习 if、for、函数参数和返回值,暂时跳过与当前数据处理无关的高级语法。
- 深入教程中文
Python 3.12:错误和异常 (新标签页)↗
阅读语法错误、运行异常及异常处理,练习保留错误类型并避免用宽泛捕获掩盖程序缺陷。
- 实践实践指南中文
Python 中文教程:给已经会 Java 的学习者 (新标签页)↗
先读第 5、6、7、8、12 章,循环语法按需回查。小练习:读入一份自己写的 JSON 学习记录,按主题统计条目数,再为缺少文件的情况给出清楚提示。 教程公开,面向已有编程概念的读者;
/3/ 链接随版本更新,学习时应选择与本机 Python 一致的版本。 - 深入参考文档英文
OpenJDK 17 LinkedHashMap 源码 (新标签页)↗
按指定版本定位
LinkedHashMap.newNode/afterNodeInsertion;对照源码分析理解实际调用边界。