列表与字典:存储你的第一个Lot数据
列表与字典:存储你的第一个Lot数据
一、问题背景:为什么需要容器类型?
上次我们讲了数据类型,用变量存储单个数据。但实际工作中,一个Lot的信息远不止这些:
```
Lot ID: FAB-ETCH-2026-001
工序: ETCH
Wafer数量: 25
厚度数据: [1250.5, 1248.3, 1251.2, 1249.8, ...] # 25个值
缺陷位置: [(2, 3), (5, 7), (12, 8), ...] # 每个缺陷的坐标
设备列表: ["AMAT-01", "AMAT-02", "LAM-03"] # 可用的设备
```
如果每个数据都单独存一个变量,你会疯掉的:
- `thickness_1`, `thickness_2`, ... `thickness_25`
- `defect_x_1`, `defect_y_1`, `defect_x_2`, `defect_y_2`, ...
更糟糕的是,如果Wafer数量是动态的(有时25片,有时24片),你怎么管理?
我刚入行的时候就有过这个教训——当时用Excel VBA写了一批数据处理脚本,把每片Wafer的厚度存成独立变量`wafer1, wafer2, ... wafer25`。有一天客户说"我们的8寸线改成12寸了,Wafer变成24片",我硬是改了一整天才把所有脚本适配过来。如果当初用列表,改一个数字就完了。
解决方案:使用容器类型,一次性存储多个相关数据。
Python提供了两种核心容器类型:
- 列表(list):有序、可变、适合存储同类型数据
- 字典(dict):键值对、无序、适合存储关联数据
这篇文章,我会用半导体FAB的真实场景,带你深入理解这两种容器类型。
---
二、技术原理:列表与字典的本质
2.1 列表(list)—— 有序的可变序列
列表是Python中使用频率最高的数据结构,没有之一。它的本质是一个动态数组:可以随时添加、删除、修改元素,Python会自动管理内存。
```python
创建列表的3种方式
wafer_thickness = [1250.5, 1248.3, 1251.2, 1249.8, 1250.1] # 直接创建
lot_history = list(("PHOTO", "ETCH", "IMP", "CVD")) # 从其他序列转换
zeros = [0] * 25 # 重复创建,常用于初始化
索引访问(从0开始,支持负数倒序)
first = wafer_thickness[0] # 1250.5,第一个元素
last = wafer_thickness[-1] # 1250.1,最后一个元素
second_last = wafer_thickness[-2] # 1249.8,倒数第二个
切片(左闭右开,Python最重要的语法之一)
first_three = wafer_thickness[0:3] # [1250.5, 1248.3, 1251.2]
last_two = wafer_thickness[-2:] # [1249.8, 1250.1]
skip_one = wafer_thickness[::2] # 每隔一个取一个(奇数位)
reversed_list = wafer_thickness[::-1] # 反转
增删改
wafer_thickness.append(1249.5) # 末尾追加
wafer_thickness.insert(2, 1250.0) # 在索引2处插入
wafer_thickness.remove(1248.3) # 删除第一个值为1248.3的元素
wafer_thickness.pop(0) # 删除索引0处的元素
wafer_thickness[0] = 1251.0 # 修改索引0处的值
常用统计函数(不用循环就能算)
count = len(wafer_thickness) # 元素数量
total = sum(wafer_thickness) # 求和
average = sum(wafer_thickness) / len(wafer_thickness) # 平均值
min_val = min(wafer_thickness) # 最小值
max_val = max(wafer_thickness) # 最大值
```
为什么索引从0开始?
这是编程语言的惯例。索引表示"偏移量"——第一个元素相对于起始地址的偏移是0,第二个是1。理解这一点很重要,因为切片`[0:3]`取的是索引0、1、2共3个元素(不包括3)。
2.2 列表推导式 —— 列表的"核武器"
列表推导式(List Comprehension)是Python区别于其他语言的最优雅语法之一。一行代码完成"遍历+过滤+转换"三件事:
```python
基础用法:对每个元素做操作
thicknesses = [1250.5, 1248.3, 1251.2, 1249.8]
把Å转成nm(除以10)
thicknesses_nm = [t / 10 for t in thicknesses] # [125.05, 124.83, 125.12, 124.98]
带条件过滤:只保留超出规格的值
spec_min, spec_max = 1240, 1260
out_of_spec = [t for t in thicknesses if t < spec_min or t > spec_max]
实战案例:从MES原始数据提取厚度
raw_data = ["T:1250.5", "T:1248.3", "T:ERROR", "T:1251.2"]
用推导式+错误处理提取有效厚度值
thicknesses = []
for item in raw_data:
try:
val = float(item.split(":")[1])
thicknesses.append(val)
except (ValueError, IndexError):
pass # 跳过无效数据
嵌套推导:二维数据展平
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [x for row in matrix for x in row] # [1, 2, 3, 4, 5, 6]
```
我在FAB项目中,列表推导式用得最多的场景是数据清洗——MES导出的原始数据里混杂着空值、错误码、单位后缀,用推导式一行就能过滤出有效数据。
2.3 字典(dict)—— 键值对的无序集合
如果说列表是"一排柜子用编号找东西",字典就是"贴了标签的柜子用名字找东西"。
```python
创建字典
lot_info = {
"lot_id": "FAB-ETCH-2026-001",
"process": "ETCH",
"wafer_count": 25,
"thickness_avg": 1250.3,
"is_completed": False,
}
访问值的两种方式
lot_id = lot_info["lot_id"] # 直接访问,键不存在会报KeyError
thickness = lot_info.get("thickness", 0) # 安全访问,键不存在返回默认值0
修改和新增
lot_info["thickness_avg"] = 1251.0 # 修改已有键
lot_info["operator"] = "张三" # 新增键值对
删除
del lot_info["operator"] # 删除键值对
removed = lot_info.pop("is_completed") # 删除并返回值
遍历(3种方式)
for key in lot_info: # 只遍历键
print(key)
for value in lot_info.values(): # 只遍历值
print(value)
for key, value in lot_info.items(): # 同时遍历键和值(最常用)
print(f"{key}: {value}")
字典合并(Python 3.9+)
extra = {"yield_rate": 95.2, "cycle_time": 45}
merged = lot_info | extra # 合并两个字典
```
字典 vs 列表的选择原则:
- 数据是"一排同类型的东西"(25片Wafer的厚度)→ 用列表
- 数据是"一个实体的多个属性"(Lot的ID、工序、数量)→ 用字典
- 不确定?问自己:"我需要用名字还是用编号来访问?"
2.4 组合使用:嵌套结构
实际工作中,列表和字典几乎永远是嵌套使用的。一个Lot的完整数据结构就是"字典里面套列表和字典":
```python
lot = {
"basic_info": {
"lot_id": "FAB-ETCH-2026-001",
"process": "ETCH",
"wafer_count": 25,
},
"thickness_data": [1250.5, 1248.3, 1251.2, 1249.8, 1250.1],
"defects": [
{"wafer_id": 2, "x": 3, "y": 7, "type": "Particle"},
{"wafer_id": 5, "x": 12, "y": 8, "type": "Scratch"},
],
"equipment_history": [
{"process": "PHOTO", "equip_id": "Nikon-01"},
{"process": "ETCH", "equip_id": "AMAT-01"},
],
}
访问嵌套数据:一层层剥开
lot_id = lot["basic_info"]["lot_id"] # 字典→字典
avg_thickness = sum(lot["thickness_data"]) / len(lot["thickness_data"]) # 字典→列表→计算
first_defect_type = lot["defects"][0]["type"] # 字典→列表→字典
```
我在MES数据导出项目中,整个数据模型就是基于这种嵌套结构设计的。JSON API返回的数据、数据库查询结果、Excel报表数据源,最终都会映射成这种"字典套列表"的格式。
---
三、实战案例:Lot数据分析工具
下面这个工具展示了列表和字典在FAB数据分析中的典型用法——从原始数据中提取统计指标,并用3-sigma原则判断是否有异常Wafer。
```python
"""
Lot数据分析工具
功能:从MES导出数据,计算统计指标,识别异常Wafer
为什么这样写:
- 用字典存储Lot数据,结构清晰,和JSON格式天然兼容
- 统计计算直接用内置函数(sum/min/max),不引入第三方库
- 异常判断用3-sigma原则,这是SPC控制图的基础
"""
from dataclasses import dataclass
from typing import List, Dict
@dataclass
class SPCResult:
"""SPC分析结果(用dataclass代替字典,代码更规范)"""
lot_id: str
wafer_count: int
avg: float
std: float
min_val: float
max_val: float
uniformity: float # 均匀性 (max-min)/avg * 100%
abnormal_wafers: List[int]
def analyze_lot(lot_data: Dict) -> SPCResult:
"""分析一个Lot的厚度数据,返回SPC结果"""
basic = lot_data.get("basic_info", {})
thickness = lot_data.get("thickness_data", [])
lot_id = basic.get("lot_id", "Unknown")
if not thickness:
raise ValueError(f"Lot {lot_id} 没有厚度数据")
# 统计计算(全部用Python内置函数,零依赖)
avg = sum(thickness) / len(thickness)
variance = sum((x - avg) ** 2 for x in thickness) / len(thickness)
std = variance ** 0.5 # 标准差 = 方差的平方根
min_val, max_val = min(thickness), max(thickness)
uniformity = (max_val - min_val) / avg * 100
# 3-sigma异常判断(99.73%的正常数据在此范围内)
ucl, lcl = avg + 3 std, avg - 3 std
abnormal_wafers = [i + 1 for i, v in enumerate(thickness) if v > ucl or v < lcl]
return SPCResult(lot_id, len(thickness), avg, std,
min_val, max_val, uniformity, abnormal_wafers)
def print_report(result: SPCResult):
"""格式化输出分析报告"""
print(f"\n{'='*50}")
print(f"Lot分析报告 - {result.lot_id}")
print(f"{'='*50}")
print(f" Wafer数量: {result.wafer_count}")
print(f" 平均厚度: {result.avg:.2f} Å")
print(f" 标准差: {result.std:.2f} Å")
print(f" 最小值: {result.min_val:.2f} Å")
print(f" 最大值: {result.max_val:.2f} Å")
print(f" 均匀性: {result.uniformity:.2f}%")
print(f" UCL(3σ): {result.avg + 3*result.std:.2f} Å")
print(f" LCL(3σ): {result.avg - 3*result.std:.2f} Å")
if result.abnormal_wafers:
print(f" ⚠️ 异常Wafer: {result.abnormal_wafers}")
else:
print(f" ✅ 所有Wafer在控制限内")
print(f"{'='*50}\n")
---- 使用示例 ----
if __name__ == '__main__':
sample_lot = {
"basic_info": {"lot_id": "FAB-ETCH-2026-001", "process": "ETCH", "wafer_count": 25},
"thickness_data": [
1250.5, 1248.3, 1251.2, 1249.8, 1250.1,
1250.8, 1249.5, 1251.0, 1250.3, 1249.6,
1250.2, 1249.9, 1251.5, 1250.6, 1249.2,
1251.1, 1250.4, 1249.7, 1250.9, 1250.0,
1249.4, 1250.7, 1251.3, 1250.2, 1249.8,
],
}
result = analyze_lot(sample_lot)
print_report(result)
```
代码说明:
- 为什么用dataclass? 比`dict`更规范——字段类型固定,IDE能自动补全,访问用`.avg`比`["avg"]`更方便
- 为什么手动算标准差? 避免引入`statistics`模块,降低依赖。实际项目中推荐用`numpy.std()`
- 3-sigma原理:正态分布中99.73%的数据在均值±3σ范围内,超出这个范围的Wafer大概率有问题
---
四、效果对比
| 维度 | 散乱变量(25个独立变量) | 列表+字典 | 提升 |
|------|------------------------|----------|------|
| 代码行数 | 200行(逐个赋值/读取) | 40行(循环+索引) | -80% |
| 数据访问可读性 | `wafer_thickness_23`(数字编号) | `thickness[22]`(简洁) | +90% |
| 扩展性(Wafer数量变化) | 改25处代码 | 改1个数据源 | +2400% |
| 序列化(保存到文件) | 需手动拼接字符串 | `json.dump()`一行搞定 | ∞ |
| 数据导入(从MES加载) | 逐个解析映射 | `dict.update()`批量更新 | +200% |
| 批量统计(均值/极值) | 写25行求和再除 | `sum()/min()/max()`一行 | ∞ |
| 数据传递给其他函数 | 传25个参数 | 传一个字典/列表 | +24倍 |
我在项目中的真实效果:
重构MES数据导出工具时,把25个独立变量改成字典+列表结构后:
- 代码从800行降到200行(-75%)
- 新增一条产线的数据类型只需加一个字典键,不用改函数签名
- 数据保存为JSON后,Excel打开就能看,再也不用手动复制粘贴
- 有一次客户临时加了5个检测项,我只改了数据结构定义,核心分析代码一行没动
---
五、实施建议
5.1 数据结构设计三原则
原则1:扁平优于嵌套
嵌套超过3层就很难维护了。我见过有人写成`data["lot"]["step"]["param"]["value"]`,每次访问都要写一长串。如果数据结构超过3层,考虑拆成多个字典或改用`dataclass`。
```python
✗ 过度嵌套(4层,难以维护)
data = {"lot": {"info": {"basic": {"id": "FAB-001"}}}}
lot_id = data["lot"]["info"]["basic"]["id"]
✓ 扁平结构(1-2层)
data = {"lot_id": "FAB-001", "process": "ETCH"}
lot_id = data["lot_id"]
```
原则2:列表存同质数据,字典存异质数据
这是最核心的选择标准。25个厚度值——同质(都是float)→ 列表。一个Lot的属性(ID是str,数量是int,完成状态是bool)——异质 → 字典。
原则3:键名要有业务含义
```python
✗ 看不懂
data = {"a": 1250.5, "b": 25, "c": "ETCH"}
✓ 一目了然
data = {"thickness_avg": 1250.5, "wafer_count": 25, "process": "ETCH"}
```
5.2 常见踩坑
坑1:直接用`==`比较浮点数列表
```python
✗ 可能因为浮点精度失败
if [0.1 + 0.2] == [0.3]: # False!因为0.1+0.2=0.30000000000000004
pass
✓ 用numpy.isclose或设定容差
tolerance = 0.01
if all(abs(a - b) < tolerance for a, b in zip(list1, list2)):
pass
```
坑2:修改列表时用错了方法
`append()`和`extend()`的区别是新手最容易搞混的:
- `append(x)` 把x作为一个整体加入列表
- `extend(x)` 把x中的每个元素分别加入列表
```python
a = [1, 2]
a.append([3, 4]) # [1, 2, [3, 4]]——列表里嵌了一个列表
b = [1, 2]
b.extend([3, 4]) # [1, 2, 3, 4]——展平了
```
坑3:循环中修改列表
```python
✗ 删元素时索引会错位
for i, val in enumerate(data):
if val < 0:
data.pop(i) # 第一个删掉后,后面的索引全变了
✓ 用列表推导式生成新列表(推荐)
data = [val for val in data if val >= 0]
✓ 或用filter
data = list(filter(lambda x: x >= 0, data))
```
---
六、进阶方向
6.1 集合(set)—— 去重和集合运算
当你需要去重或做交集/并集运算时,用集合:
```python
去重:找出今天生产过的所有设备(可能有重复)
equipment_ids = ["AMAT-01", "LAM-02", "AMAT-01", "TEL-03"]
unique_equip = list(set(equipment_ids)) # ["AMAT-01", "LAM-02", "TEL-03"]
集合运算:找出两批Lot都用过的设备
lot_a_equips = {"AMAT-01", "LAM-02", "TEL-03"}
lot_b_equips = {"AMAT-01", "LAM-02", "KLA-01"}
common = lot_a_equips & lot_b_equips # {"AMAT-01", "LAM-02"}——交集
all_equip = lot_a_equips | lot_b_equips # 并集
```
6.2 元组(tuple)—— 不可变的列表
元组一旦创建就不能修改,适合存储不应该被改变的数据:
```python
工艺规格(不允许运行时修改)
ETCH_SPEC = (1240.0, 1260.0) # (下限, 上限) 单位Å
PHOTO_SPEC = (95.0, 105.0) # CD规格 (nm)
函数返回多个值(Python的惯例)
def calc_stats(data):
return min(data), max(data), sum(data)/len(data)
min_val, max_val, avg = calc_stats(thickness_data)
```
6.3 collections模块 —— 进阶数据结构
Python标准库的`collections`模块提供了几个实用的数据结构:
- `defaultdict`:自动初始化默认值的字典,避免KeyError
- `Counter`:一键统计元素出现次数
- `namedtuple`:带字段名的元组,比普通元组更易读
```python
from collections import Counter
统计每种缺陷类型的数量(在FAB质量管理中非常常用)
defect_types = ["Particle", "Scratch", "Particle", "Particle", "Pattern Error"]
counts = Counter(defect_types)
print(counts.most_common(2)) # [('Particle', 3), ('Scratch', 1)]
```
6.4 字典推导式
和列表推导式一样优雅,适合从现有数据生成字典:
```python
从原始数据生成 {wafer_id: thickness} 的映射
thickness_data = [1250.5, 1248.3, 1251.2]
thickness_map = {f"W{i+1:02d}": t for i, t in enumerate(thickness_data)}
{'W01': 1250.5, 'W02': 1248.3, 'W03': 1251.2}
翻转字典的键和值
inverted = {v: k for k, v in thickness_map.items()}
```
---
> 📦 专栏VIP资源包:包含本系列40篇全部可运行源码、示例数据集、自动化脚本工具包。在专栏主页点击「VIP资源」即可获取。
---
七、总结
列表和字典是Python最常用的数据结构,也是后续所有数据分析工作的基础。记住三个要点:
1. 列表存同质数据(25片Wafer的厚度)——用索引或循环访问
2. 字典存异质数据(Lot的多个属性)——用键名访问,代码自解释
3. 组合嵌套使用(字典里套列表)——这是FAB数据最真实的结构
下一篇我们将学习《循环与判断:自动化处理批量数据》,用for循环批量处理一个Lot的25片Wafer数据。
---
> 💬 你在工作中用Python处理过什么批量数据?有没有遇到什么坑?欢迎在评论区聊聊!
>
> 📚 专栏持续更新中,关注不迷路。收藏+点赞支持一下~ 👍
>
> 🔧 专栏配套工具包(含本篇完整可运行代码+示例数据)已上传为VIP资源,专栏目录页可下载。





