本节摘要:验完食材,得选对盛器。本节对照列表、字典、集合、元组四种容器的脾气,给出"要装什么、怎么取用"的选型口诀,示范增删查改的日常操作,并把数组的基本算术(加减乘除、幂、整除、取模)讲出"逐元素"的直觉。承接 1.1 的验收,通往第 2 章择菜间。
别以为盛器的选择无关紧要——同一批订单号,装在列表里查一次要翻遍全表,装在字典或集合里一眼就能定位;该不许改动的配置清单用了列表,深夜就会有人往里 append 一个坏值。本节承接 1.1 的验收动作:食材认全之后,第二件事是给它们安排合适的容器。这一节的选型口诀会一直用到第 6 章(函数式工具靠字典映射派活)和第 7 章(NumPy 数组的批量算术),是备料间的收尾科目。
**列表(list)**有序、可变、允许重复,是"按顺序排队"的默认选择:时间序列的采样点、要逐个处理的文件名清单。**字典(dict)**按键取值,一步到位:用户 ID 到用户名的映射、配置项到默认值的对照表。**集合(set)**自动去重、判存在极快:黑名单、要剔除的重复单号。**元组(tuple)**有序但不可变:坐标点、函数返回的多个值、不该被改的常量序列。选型口诀一句话——要顺序用列表,要映射用字典,要唯一用集合,要不可变用元组。
order_ids = ["A102", "A103", "A102", "A105"] # 列表:保留原始顺序与重复 id_set = set(order_ids) # 集合:自动去重 name_map = {"A102": "张三", "A103": "李四"} # 字典:编号到姓名的映射 point = (3, 7) # 元组:坐标,不该被改 print(len(id_set), id_set) # 3 {'A102', 'A103', 'A105'} print(name_map[order_ids[0]]) # 张三 print("A105" in id_set) # True,存在性判断一步到位 # point[0] = 9 # TypeError,元组不许改,这正是它的价值
四种盛器的日常操作值得练到肌肉记忆。列表的 append、extend、insert、pop;字典的取值带默认值 get、合并 update;集合的并集交集差集。而把"建一个容器"写漂亮的,是推导式——数据处理里最常用的表达,第 6 章的 map 本质上就是它的函数版。
raw = [" Alice ", "bob", " CARL", "bob"] names = [x.strip().title() for x in raw] # 列表推导式:清洗一行写完 length_map = {x: len(x) for x in names} # 字典推导式:值到长度的映射 long_names = {x for x in names if len(x) > 3} # 集合推导式:挑出长名字 print(names) # ['Alice', 'Bob', 'Carl', 'Bob'] print(length_map) # {'Alice': 5, 'Bob': 3, 'Carl': 4} print(long_names) # {'Alice', 'Carl'}
NumPy 数组是本册后半段的常客,它最大的脾气是逐元素运算:两个形状相同的数组相加,是位置对位置地加;数组和标量运算,是每个元素都和这个标量算一遍。这与列表的行为截然不同——列表加列表是拼接,列表乘数字是重复。把这条直觉立住,第 7 章的向量化才不玄。
import numpy as np qty = np.array([10, 25, 40, 5]) price = np.array([2.5, 8.0, 3.2, 12.0]) amount = qty * price # 逐元素相乘:单价数量点乘得金额 print(amount) # [ 25. 200. 128. 60.] print(amount ** 2) # 每个元素平方 print(amount // 10, amount % 7) # 整除、取模,同样逐元素 print((amount - amount.min()) / (amount.max() - amount.min())) # 手工归一化 # [0. 0.896552 0.5 1. ] <- 这正是 2.5 节 min-max 归一化的雏形
顺带立一条规矩:数组运算快,不是因为写了更短的代码,而是因为循环交给了底层的批量执行。同一件事,Python 循环写一遍是家常小灶,数组算一遍是中央厨房的流水线——这条道理第 8 章(效率优化)会拿出实测数据。
**翻车一:引用别名,改一个动一双。**把列表赋值给另一个变量,复制的是"地址"不是内容。备菜时把同一盆料当成两盆,动了一盆另一盆跟着少——这类事故在函数传参时尤其常见。
a = [1, 2, 3] b = a # 只是别名,不是新盆 b.append(4) print(a) # [1, 2, 3, 4] <- a 也变了 c = a.copy() # 浅拷贝:新盆(嵌套结构需 copy.deepcopy) c.clear() print(a, c) # [1, 2, 3, 4] []
**翻车二:可变默认参数。**def f(x, box=[]) 里的空列表在函数定义时就造好了,之后每次调用共用同一只盆。正确写法是默认 None、函数体内再建新列表。
**翻车三:集合无序。**set 打印顺序不可预测,靠它在"去掉重复后保住先后"必翻车——保序去重要用 dict.fromkeys(items) 这类手法,第 2 章去重一节会正面处理。
标准库的 collections 模块是内置盛器的加料版:Counter 一行完成计数(词频、类目分布),defaultdict(list) 免去"键不存在先建空列表"的样板,deque 在头部插删比列表快。pandas 的 Index 对象则介于数组与集合之间——既支持下标又支持交集并集,是后面章节合并对位的底座。
from collections import Counter, defaultdict orders = ["咖啡", "奶茶", "咖啡", "果汁", "咖啡"] print(Counter(orders)) # Counter({'咖啡': 3, '奶茶': 1, '果汁': 1}) groups = defaultdict(list) for drink, user in [("咖啡", "u1"), ("奶茶", "u2"), ("咖啡", "u3")]: groups[drink].append(user) print(dict(groups)) # {'咖啡': ['u1', 'u3'], '奶茶': ['u2']}
备料间到此收档。下一章进择菜间,从出场率最高的缺失值开始——dropna 与 fillna 这对双子星,是清洗工序的第一道主菜。