Python数据结构
数据结构决定数据如何保存、查找、遍历和修改。Python 最常用的数据结构是 list、tuple、dict、set。掌握它们后,才能写出简洁且性能合理的代码。
为什么要先学数据结构
同样一批数据,用错结构会让代码变复杂,性能也会变差。比如按用户 ID 查用户,如果用 list,每次都要从头遍历;如果用 dict,可以直接按 key 查找。比如标签去重,如果用 list 要手动判断是否存在;如果用 set,天然就能去重。
数据结构不是语法细节,而是解决问题的建模方式。你选择了什么结构,就决定了后续代码怎么查、怎么改、怎么判重、怎么遍历。
四种结构对比
| 类型 | 是否有序 | 是否可变 | 是否允许重复 | 典型用途 |
|---|---|---|---|---|
list | 有序 | 可变 | 允许 | 保存一组数据、列表页结果 |
tuple | 有序 | 不可变 | 允许 | 固定组合、函数多返回值 |
dict | 插入有序 | 可变 | key 不重复 | 根据 key 查找 value |
set | 无序 | 可变 | 不允许 | 去重、交并差集合运算 |
如何选择
flowchart TD
A["需要保存多个值"] --> B{"是否按 key 查找"}
B -- "是" --> C["使用 dict"]
B -- "否" --> D{"是否需要去重"}
D -- "是" --> E["使用 set"]
D -- "否" --> F{"是否允许修改"}
F -- "否" --> G["使用 tuple"]
F -- "是" --> H["使用 list"]底层原理简化理解
| 类型 | 简化原理 | 适合的访问方式 |
|---|---|---|
list | 连续顺序容器,按位置保存元素 | 按下标访问、按顺序遍历 |
tuple | 不可变顺序容器 | 表达固定组合 |
dict | 哈希表,按 key 映射到 value | 按 key 快速查找 |
set | 只保存 key 的哈希表 | 去重、成员判断、集合运算 |
这就是为什么 dict["id"] 通常比在 list 里循环查找快。dict 和 set 依赖哈希,所以 key 必须是可哈希对象,例如字符串、数字、元组;list 和 dict 这类可变对象不能直接作为 dict 的 key。
时间复杂度先建立感觉
复杂度不是数学炫技,而是估算“数据量变大后会不会炸”。零基础先记住几个常见操作:
| 操作 | 常见复杂度 | 解释 |
|---|---|---|
list[index] | O(1) | 列表按位置直接访问 |
x in list | O(n) | 可能从头扫到尾 |
list.append(x) | 平均 O(1) | 大多数时候直接追加,偶尔扩容 |
list.insert(0, x) | O(n) | 前面插入要搬动后面元素 |
dict[key] | 平均 O(1) | 通过 hash 定位 |
key in dict | 平均 O(1) | 判断 key 是否存在很快 |
x in set | 平均 O(1) | set 本质像只存 key 的 dict |
排序 sorted(list) | O(n log n) | 排序比单次遍历贵 |
举个业务例子:有 10 万个资产编号,要判断导入文件里某个编号是否已经存在。
错误思路:
existing_codes = ["A001", "A002", "A003"]
def exists(code: str) -> bool:
return code in existing_codes如果 existing_codes 是 list,code in existing_codes 可能每次都扫很多元素。导入 10 万行时,就可能变成“10 万行 * 每次扫很多行”。
更适合:
existing_codes = {"A001", "A002", "A003"}
def exists(code: str) -> bool:
return code in existing_codesset 的成员判断平均更快,适合去重、判重、黑名单、白名单、权限集合。
list 列表
列表适合保存一组有顺序的数据。
users = ["Tom", "Alice", "Bob"]
users.append("Jerry")
users.remove("Alice")
print(users[0])
print(len(users))常用操作:
| 操作 | 示例 |
|---|---|
| 追加 | users.append("Tom") |
| 插入 | users.insert(0, "Tom") |
| 删除 | users.remove("Tom") |
| 按下标删除 | users.pop(0) |
| 排序 | users.sort() |
| 切片 | users[0:2] |
列表推导式:
nums = [1, 2, 3, 4, 5]
even_nums = [num for num in nums if num % 2 == 0]list 为什么按下标快、头部插入慢
可以把 list 简化理解成一排连续格子:
flowchart TD
A["list 对象"] --> B["第0个元素"]
A --> C["第1个元素"]
A --> D["第2个元素"]
A --> E["第3个元素"]按下标访问时,Python 能很快找到第几个位置;但如果在开头插入一个元素,后面的元素位置都要往后挪。
items = [2, 3, 4]
items.insert(0, 1)这会把原来的 2、3、4 都往后移动。如果频繁从头部插入或弹出,应该考虑 collections.deque。
from collections import deque
queue = deque()
queue.append("task-1")
queue.append("task-2")
first = queue.popleft()deque 适合队列场景,例如任务队列、日志缓冲、滑动窗口。
tuple 元组
元组创建后不能修改,适合表达固定结构。
point = (10, 20)
x, y = point函数返回多个值时,经常使用元组解包:
def get_user():
return "Tom", 18
name, age = get_user()适合使用元组的场景:
- 坐标、范围、固定配置。
- 函数返回多个简单值。
- 不希望调用方修改的数据。
tuple 不是“只读 list”这么简单
tuple 的价值在于表达“结构固定”。例如一个坐标 (x, y)、一个数据库连接配置 (host, port)、一个统计结果 (success_count, failed_count)。
def count_result(rows: list[dict[str, str]]) -> tuple[int, int]:
success = 0
failed = 0
for row in rows:
if row.get("status") == "SUCCESS":
success += 1
else:
failed += 1
return success, failed如果字段越来越多,tuple 就不够清晰了,应改成 dataclass:
from dataclasses import dataclass
@dataclass(frozen=True)
class ImportSummary:
success_count: int
failed_count: intfrozen=True 表示对象创建后不希望被修改,适合统计结果、配置快照、不可变 DTO。
dict 字典
字典是 Python 中最重要的数据结构之一,用于按 key 查找 value。
user = {
"id": 1001,
"name": "Tom",
"age": 18,
}
print(user["name"])
print(user.get("email", "未填写"))遍历字典:
for key, value in user.items():
print(key, value)字典嵌套:
order = {
"order_id": "O1001",
"user": {
"id": 1001,
"name": "Tom",
},
"items": [
{"name": "book", "price": 30},
{"name": "pen", "price": 5},
],
}读取嵌套数据时要注意 key 是否存在。
dict 的原理:为什么按 key 查找快
dict 可以简化理解成“key 经过 hash 计算后,找到一个位置保存 value”。
flowchart TD
A["key: asset_code"] --> B["hash(key)"]
B --> C["定位到哈希表槽位"]
C --> D{"槽位是否匹配"}
D -- "匹配" --> E["返回 value"]
D -- "冲突" --> F["继续查找下一个候选位置"]这解释了几个现象:
- dict 查 key 平均很快。
- key 必须可哈希。
- 可变对象不能作为 key,因为内容变了 hash 也可能变。
- hash 冲突不是不能处理,但冲突太多会影响性能。
dict 适合做索引映射
商业项目里经常要把一批数据转成映射,避免重复查询。
assets = [
{"asset_code": "A001", "name": "CT"},
{"asset_code": "A002", "name": "MRI"},
]
asset_map = {asset["asset_code"]: asset for asset in assets}
print(asset_map["A001"]["name"])这相当于在内存里建立一个“小索引”。如果后续要按 asset_code 查很多次,先转 dict 很划算。
不推荐:
def find_asset(code: str) -> dict | None:
for asset in assets:
if asset["asset_code"] == code:
return asset
return None如果只查一次,循环可以接受;如果查几万次,就应该先构建 dict。
defaultdict 和 Counter
统计和分组是 Python 中 dict 的高频场景。
统计状态数量:
from collections import Counter
statuses = ["SUCCESS", "FAILED", "SUCCESS", "RUNNING"]
counter = Counter(statuses)
print(counter["SUCCESS"]) # 2按科室分组资产:
from collections import defaultdict
rows = [
{"dept": "D01", "asset_code": "A001"},
{"dept": "D01", "asset_code": "A002"},
{"dept": "D02", "asset_code": "A003"},
]
grouped: dict[str, list[dict[str, str]]] = defaultdict(list)
for row in rows:
grouped[row["dept"]].append(row)defaultdict(list) 的意思是:如果 key 不存在,就自动创建一个空 list。它适合分组,但不要滥用到所有场景;有些业务 key 不存在应该报错,而不是悄悄创建默认值。
set 集合
集合用于去重和集合运算。
tags = {"python", "ai", "web"}
tags.add("data")
tags.add("python")
print(tags)集合运算:
a = {1, 2, 3}
b = {3, 4, 5}
print(a & b) # 交集 {3}
print(a | b) # 并集 {1, 2, 3, 4, 5}
print(a - b) # 差集 {1, 2}常见场景:
- 用户标签去重。
- 两组用户求交集。
- 判断某个值是否出现过。
set 的商业用法:导入去重和权限判断
导入文件时经常要判断同一个文件里是否重复:
def find_duplicate_codes(rows: list[dict[str, str]]) -> set[str]:
seen: set[str] = set()
duplicated: set[str] = set()
for row in rows:
code = row["asset_code"].strip()
if code in seen:
duplicated.add(code)
else:
seen.add(code)
return duplicated权限判断也常用 set:
user_permissions = {"asset:read", "asset:write"}
required_permission = "asset:write"
if required_permission not in user_permissions:
raise PermissionError("无权限")集合运算适合做权限差异分析:
old_permissions = {"asset:read", "asset:write"}
new_permissions = {"asset:read", "asset:delete"}
added = new_permissions - old_permissions
removed = old_permissions - new_permissions
common = old_permissions & new_permissions可变对象和不可变对象
| 类型 | 是否可变 |
|---|---|
list | 可变 |
dict | 可变 |
set | 可变 |
tuple | 不可变 |
str | 不可变 |
int | 不可变 |
可变对象作为函数默认参数容易出问题。
错误写法:
def add_item(item, items=[]):
items.append(item)
return items正确写法:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items为什么会这样?
默认参数在函数定义时创建一次,而不是每次调用都创建。items=[] 这个 list 会被多次调用共享。
flowchart TD
A["函数定义时"] --> B["创建默认 list 对象"]
B --> C["第一次调用使用同一个 list"]
B --> D["第二次调用仍使用同一个 list"]
C --> E["append 后 list 改变"]
D --> E这类问题在生产里很隐蔽,比如默认参数里放 errors=[]、cache={},会导致不同请求共享状态。
浅拷贝和深拷贝
import copy
data = [{"name": "Tom"}]
shallow = data.copy()
deep = copy.deepcopy(data)浅拷贝只复制外层列表,里面的字典仍然共享。深拷贝会复制嵌套对象。
示例:
data = [{"name": "Tom"}]
shallow = data.copy()
shallow[0]["name"] = "Jerry"
print(data[0]["name"]) # Jerry原因是外层 list 是新的,但里面的 dict 还是同一个对象。
什么时候需要深拷贝:
| 场景 | 建议 |
|---|---|
| 只复制一层普通 list | copy() 足够 |
| 嵌套 dict/list 且要独立修改 | copy.deepcopy() |
| 数据量很大 | 谨慎 deep copy,可能消耗大量内存 |
| 只需要读,不修改 | 不需要拷贝 |
很多时候,比深拷贝更好的方式是重新构造你真正需要的结果,而不是整份复制。
数据处理示例
统计订单中每个商品的购买数量:
orders = [
{"user": "Tom", "item": "book"},
{"user": "Alice", "item": "book"},
{"user": "Tom", "item": "pen"},
]
counter = {}
for order in orders:
item = order["item"]
counter[item] = counter.get(item, 0) + 1
print(counter)输出:
{"book": 2, "pen": 1}商业场景 Demo:资产导入预校验
下面这个 Demo 把 list、dict、set、Counter 都放到一个真实场景里:导入医疗资产前做预校验。
需求:
- 检查文件内资产编号是否重复。
- 检查资产编号是否已经存在于系统。
- 按科室统计本次导入数量。
- 输出合格行和错误行。
flowchart TD
A["读取导入行 list"] --> B["用 set 检查文件内重复"]
B --> C["用 existing_codes set 检查库内重复"]
C --> D["用 dict/list 保存错误明细"]
D --> E["用 Counter 统计科室数量"]
E --> F["返回 ImportPreview"]代码:
from collections import Counter
from dataclasses import dataclass
@dataclass
class ImportPreview:
valid_rows: list[dict[str, str]]
error_rows: list[dict[str, str]]
dept_counter: Counter[str]
def preview_import(
rows: list[dict[str, str]],
existing_codes: set[str],
) -> ImportPreview:
seen_codes: set[str] = set()
valid_rows: list[dict[str, str]] = []
error_rows: list[dict[str, str]] = []
dept_counter: Counter[str] = Counter()
for index, row in enumerate(rows, start=1):
code = row.get("asset_code", "").strip()
dept = row.get("dept_code", "").strip()
errors: list[str] = []
if not code:
errors.append("资产编号不能为空")
elif code in seen_codes:
errors.append("文件内资产编号重复")
elif code in existing_codes:
errors.append("资产编号已存在")
if not dept:
errors.append("科室不能为空")
if errors:
error_row = dict(row)
error_row["line_no"] = str(index)
error_row["errors"] = ";".join(errors)
error_rows.append(error_row)
continue
seen_codes.add(code)
dept_counter[dept] += 1
valid_rows.append(row)
return ImportPreview(
valid_rows=valid_rows,
error_rows=error_rows,
dept_counter=dept_counter,
)调用:
rows = [
{"asset_code": "A001", "dept_code": "D01"},
{"asset_code": "A002", "dept_code": "D01"},
{"asset_code": "A001", "dept_code": "D02"},
{"asset_code": "", "dept_code": "D03"},
]
preview = preview_import(rows, existing_codes={"A009"})
print(len(preview.valid_rows))
print(preview.error_rows)
print(preview.dept_counter)这里每个数据结构都有明确职责:
| 结构 | 在 Demo 中的职责 |
|---|---|
list[dict] | 保存导入行、合格行、错误行 |
set | 快速判断编号是否出现过 |
dict | 表示一行结构化数据和错误明细 |
Counter | 统计每个科室导入数量 |
dataclass | 表达返回结果结构 |
如果不用 set,而是每次在已处理 list 中循环找重复,导入行数上来后会明显变慢。
嵌套 dict 什么时候该换成 dataclass
初学者很喜欢所有东西都用 dict:
asset = {
"asset_code": "A001",
"name": "CT",
"owner": {
"dept_code": "D01",
"dept_name": "影像科",
},
}dict 很灵活,但也有代价:
| 问题 | 后果 |
|---|---|
| key 写错 | 运行到那一行才报错 |
| 嵌套太深 | 读取和修改都困难 |
| 字段含义不清 | 新人不知道有哪些字段 |
| 到处传 dict | 业务规则散落 |
如果结构稳定,建议用 dataclass:
from dataclasses import dataclass
@dataclass
class AssetOwner:
dept_code: str
dept_name: str
@dataclass
class Asset:
asset_code: str
name: str
owner: AssetOwner使用:
asset = Asset(
asset_code="A001",
name="CT",
owner=AssetOwner(dept_code="D01", dept_name="影像科"),
)选择原则:
| 场景 | 建议 |
|---|---|
| 临时 JSON 数据 | dict |
| 字段稳定的业务对象 | dataclass |
| 需要行为和校验 | 普通类或 Pydantic 模型 |
| Web API 入参出参 | Pydantic 模型 |
生产排查:数据结构用错会怎样
flowchart TD
A["程序变慢或内存升高"] --> B{"是否大量循环查找"}
B -- "是" --> C["list 查找改 dict/set"]
B -- "否" --> D{"是否一次加载大量数据"}
D -- "是" --> E["改分批、生成器、流式处理"]
D -- "否" --> F{"是否频繁深拷贝"}
F -- "是" --> G["减少复制,重构数据流"]
F -- "否" --> H["检查外部 IO 或算法"]常见现象:
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 导入越跑越慢 | list 里反复 in 判断 | 改用 set 判重 |
| 内存突然很高 | fetchall() 后又构造多个大 list | 分批处理,避免复制 |
| 数据被意外修改 | 多个变量引用同一个可变对象 | 明确 copy 或构造新对象 |
| 返回字段经常拼错 | dict 到处传 | 使用 dataclass/Pydantic |
| 去重后顺序乱 | 使用 set 后又要求原顺序 | 用 dict 保序去重或 list + set 组合 |
保序去重示例:
def unique_keep_order(values: list[str]) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for value in values:
if value not in seen:
seen.add(value)
result.append(value)
return result面试标准回答
list、tuple、dict、set 怎么选
list 适合有序、可变、允许重复的数据;tuple 适合固定组合和不希望修改的数据;dict 适合按 key 快速查 value;set 适合去重、成员判断和集合运算。选择时要看访问方式:按顺序处理用 list,按 key 查找用 dict,判重用 set,固定结构用 tuple 或 dataclass。dict 为什么查询快
dict 底层基于哈希表思想。key 经过 hash 计算后定位到哈希表中的位置,平均情况下查找接近 O(1)。所以按 ID、编码、用户名查数据时,dict 通常比在 list 中循环查找更快。但 dict 的 key 必须可哈希,可变对象如 list、dict 不能作为 key。可变对象默认参数为什么危险
函数默认参数在函数定义时创建一次,不是每次调用都创建。如果默认参数是 list 或 dict,多次调用会共享同一个对象,导致不同调用之间互相污染。正确做法是默认值写 None,在函数内部新建 list 或 dict。浅拷贝和深拷贝区别
浅拷贝只复制外层容器,里面的嵌套对象仍然共享;深拷贝会递归复制嵌套对象。嵌套 list/dict 需要独立修改时才考虑 deepcopy,但大数据量深拷贝成本高,很多时候应该重新构造需要的数据。常见问题
| 问题 | 原因 | 建议 |
|---|---|---|
KeyError | 字典 key 不存在 | 使用 get() 或先判断 |
IndexError | 列表下标越界 | 访问前检查长度 |
| 数据被意外修改 | 多个变量引用同一可变对象 | 需要时使用拷贝 |
| 集合顺序不稳定 | set 不保证业务顺序 | 需要顺序用 list |
| 嵌套太深难维护 | 结构过复杂 | 使用 dataclass 或类 |
练习
- 用 list 保存 5 个用户姓名,并遍历输出。
- 用 dict 保存用户信息,安全读取不存在的字段。
- 用 set 对标签列表去重。
- 统计一个字符串中每个字符出现的次数。
- 把订单列表按用户 ID 分组,生成
dict[user_id, orders]。
小结
Python 数据结构的选择要看访问方式:按顺序处理用 list,固定组合用 tuple,按 key 查找用 dict,去重和集合运算用 set。写业务代码时,dict + list 的组合最常见,但嵌套过深时要及时抽象成类或 dataclass。
