Skip to content

Python数据结构

数据结构决定数据如何保存、查找、遍历和修改。Python 最常用的数据结构是 listtupledictset。掌握它们后,才能写出简洁且性能合理的代码。

为什么要先学数据结构

同样一批数据,用错结构会让代码变复杂,性能也会变差。比如按用户 ID 查用户,如果用 list,每次都要从头遍历;如果用 dict,可以直接按 key 查找。比如标签去重,如果用 list 要手动判断是否存在;如果用 set,天然就能去重。

数据结构不是语法细节,而是解决问题的建模方式。你选择了什么结构,就决定了后续代码怎么查、怎么改、怎么判重、怎么遍历。

四种结构对比

类型是否有序是否可变是否允许重复典型用途
list有序可变允许保存一组数据、列表页结果
tuple有序不可变允许固定组合、函数多返回值
dict插入有序可变key 不重复根据 key 查找 value
set无序可变不允许去重、交并差集合运算

如何选择

mermaid
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 listO(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 万个资产编号,要判断导入文件里某个编号是否已经存在。

错误思路:

python
existing_codes = ["A001", "A002", "A003"]

def exists(code: str) -> bool:
    return code in existing_codes

如果 existing_codes 是 list,code in existing_codes 可能每次都扫很多元素。导入 10 万行时,就可能变成“10 万行 * 每次扫很多行”。

更适合:

python
existing_codes = {"A001", "A002", "A003"}

def exists(code: str) -> bool:
    return code in existing_codes

set 的成员判断平均更快,适合去重、判重、黑名单、白名单、权限集合。

list 列表

列表适合保存一组有顺序的数据。

python
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]

列表推导式:

python
nums = [1, 2, 3, 4, 5]
even_nums = [num for num in nums if num % 2 == 0]

list 为什么按下标快、头部插入慢

可以把 list 简化理解成一排连续格子:

mermaid
flowchart TD
    A["list 对象"] --> B["第0个元素"]
    A --> C["第1个元素"]
    A --> D["第2个元素"]
    A --> E["第3个元素"]

按下标访问时,Python 能很快找到第几个位置;但如果在开头插入一个元素,后面的元素位置都要往后挪。

python
items = [2, 3, 4]
items.insert(0, 1)

这会把原来的 2、3、4 都往后移动。如果频繁从头部插入或弹出,应该考虑 collections.deque

python
from collections import deque

queue = deque()
queue.append("task-1")
queue.append("task-2")
first = queue.popleft()

deque 适合队列场景,例如任务队列、日志缓冲、滑动窗口。

tuple 元组

元组创建后不能修改,适合表达固定结构。

python
point = (10, 20)
x, y = point

函数返回多个值时,经常使用元组解包:

python
def get_user():
    return "Tom", 18


name, age = get_user()

适合使用元组的场景:

  1. 坐标、范围、固定配置。
  2. 函数返回多个简单值。
  3. 不希望调用方修改的数据。

tuple 不是“只读 list”这么简单

tuple 的价值在于表达“结构固定”。例如一个坐标 (x, y)、一个数据库连接配置 (host, port)、一个统计结果 (success_count, failed_count)

python
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

python
from dataclasses import dataclass


@dataclass(frozen=True)
class ImportSummary:
    success_count: int
    failed_count: int

frozen=True 表示对象创建后不希望被修改,适合统计结果、配置快照、不可变 DTO。

dict 字典

字典是 Python 中最重要的数据结构之一,用于按 key 查找 value。

python
user = {
    "id": 1001,
    "name": "Tom",
    "age": 18,
}

print(user["name"])
print(user.get("email", "未填写"))

遍历字典:

python
for key, value in user.items():
    print(key, value)

字典嵌套:

python
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”。

mermaid
flowchart TD
    A["key: asset_code"] --> B["hash(key)"]
    B --> C["定位到哈希表槽位"]
    C --> D{"槽位是否匹配"}
    D -- "匹配" --> E["返回 value"]
    D -- "冲突" --> F["继续查找下一个候选位置"]

这解释了几个现象:

  1. dict 查 key 平均很快。
  2. key 必须可哈希。
  3. 可变对象不能作为 key,因为内容变了 hash 也可能变。
  4. hash 冲突不是不能处理,但冲突太多会影响性能。

dict 适合做索引映射

商业项目里经常要把一批数据转成映射,避免重复查询。

python
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 很划算。

不推荐:

python
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 的高频场景。

统计状态数量:

python
from collections import Counter

statuses = ["SUCCESS", "FAILED", "SUCCESS", "RUNNING"]
counter = Counter(statuses)

print(counter["SUCCESS"])  # 2

按科室分组资产:

python
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 集合

集合用于去重和集合运算。

python
tags = {"python", "ai", "web"}
tags.add("data")
tags.add("python")

print(tags)

集合运算:

python
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 的商业用法:导入去重和权限判断

导入文件时经常要判断同一个文件里是否重复:

python
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:

python
user_permissions = {"asset:read", "asset:write"}
required_permission = "asset:write"

if required_permission not in user_permissions:
    raise PermissionError("无权限")

集合运算适合做权限差异分析:

python
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不可变

可变对象作为函数默认参数容易出问题。

错误写法:

python
def add_item(item, items=[]):
    items.append(item)
    return items

正确写法:

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

为什么会这样?

默认参数在函数定义时创建一次,而不是每次调用都创建。items=[] 这个 list 会被多次调用共享。

mermaid
flowchart TD
    A["函数定义时"] --> B["创建默认 list 对象"]
    B --> C["第一次调用使用同一个 list"]
    B --> D["第二次调用仍使用同一个 list"]
    C --> E["append 后 list 改变"]
    D --> E

这类问题在生产里很隐蔽,比如默认参数里放 errors=[]cache={},会导致不同请求共享状态。

浅拷贝和深拷贝

python
import copy

data = [{"name": "Tom"}]

shallow = data.copy()
deep = copy.deepcopy(data)

浅拷贝只复制外层列表,里面的字典仍然共享。深拷贝会复制嵌套对象。

示例:

python
data = [{"name": "Tom"}]
shallow = data.copy()

shallow[0]["name"] = "Jerry"
print(data[0]["name"])  # Jerry

原因是外层 list 是新的,但里面的 dict 还是同一个对象。

什么时候需要深拷贝:

场景建议
只复制一层普通 listcopy() 足够
嵌套 dict/list 且要独立修改copy.deepcopy()
数据量很大谨慎 deep copy,可能消耗大量内存
只需要读,不修改不需要拷贝

很多时候,比深拷贝更好的方式是重新构造你真正需要的结果,而不是整份复制。

数据处理示例

统计订单中每个商品的购买数量:

python
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)

输出:

python
{"book": 2, "pen": 1}

商业场景 Demo:资产导入预校验

下面这个 Demo 把 listdictsetCounter 都放到一个真实场景里:导入医疗资产前做预校验。

需求:

  1. 检查文件内资产编号是否重复。
  2. 检查资产编号是否已经存在于系统。
  3. 按科室统计本次导入数量。
  4. 输出合格行和错误行。
mermaid
flowchart TD
    A["读取导入行 list"] --> B["用 set 检查文件内重复"]
    B --> C["用 existing_codes set 检查库内重复"]
    C --> D["用 dict/list 保存错误明细"]
    D --> E["用 Counter 统计科室数量"]
    E --> F["返回 ImportPreview"]

代码:

python
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,
    )

调用:

python
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:

python
asset = {
    "asset_code": "A001",
    "name": "CT",
    "owner": {
        "dept_code": "D01",
        "dept_name": "影像科",
    },
}

dict 很灵活,但也有代价:

问题后果
key 写错运行到那一行才报错
嵌套太深读取和修改都困难
字段含义不清新人不知道有哪些字段
到处传 dict业务规则散落

如果结构稳定,建议用 dataclass

python
from dataclasses import dataclass


@dataclass
class AssetOwner:
    dept_code: str
    dept_name: str


@dataclass
class Asset:
    asset_code: str
    name: str
    owner: AssetOwner

使用:

python
asset = Asset(
    asset_code="A001",
    name="CT",
    owner=AssetOwner(dept_code="D01", dept_name="影像科"),
)

选择原则:

场景建议
临时 JSON 数据dict
字段稳定的业务对象dataclass
需要行为和校验普通类或 Pydantic 模型
Web API 入参出参Pydantic 模型

生产排查:数据结构用错会怎样

mermaid
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 组合

保序去重示例:

python
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 怎么选

text
list 适合有序、可变、允许重复的数据;tuple 适合固定组合和不希望修改的数据;dict 适合按 key 快速查 value;set 适合去重、成员判断和集合运算。选择时要看访问方式:按顺序处理用 list,按 key 查找用 dict,判重用 set,固定结构用 tuple 或 dataclass。

dict 为什么查询快

text
dict 底层基于哈希表思想。key 经过 hash 计算后定位到哈希表中的位置,平均情况下查找接近 O(1)。所以按 ID、编码、用户名查数据时,dict 通常比在 list 中循环查找更快。但 dict 的 key 必须可哈希,可变对象如 list、dict 不能作为 key。

可变对象默认参数为什么危险

text
函数默认参数在函数定义时创建一次,不是每次调用都创建。如果默认参数是 list 或 dict,多次调用会共享同一个对象,导致不同调用之间互相污染。正确做法是默认值写 None,在函数内部新建 list 或 dict。

浅拷贝和深拷贝区别

text
浅拷贝只复制外层容器,里面的嵌套对象仍然共享;深拷贝会递归复制嵌套对象。嵌套 list/dict 需要独立修改时才考虑 deepcopy,但大数据量深拷贝成本高,很多时候应该重新构造需要的数据。

常见问题

问题原因建议
KeyError字典 key 不存在使用 get() 或先判断
IndexError列表下标越界访问前检查长度
数据被意外修改多个变量引用同一可变对象需要时使用拷贝
集合顺序不稳定set 不保证业务顺序需要顺序用 list
嵌套太深难维护结构过复杂使用 dataclass 或类

练习

  1. 用 list 保存 5 个用户姓名,并遍历输出。
  2. 用 dict 保存用户信息,安全读取不存在的字段。
  3. 用 set 对标签列表去重。
  4. 统计一个字符串中每个字符出现的次数。
  5. 把订单列表按用户 ID 分组,生成 dict[user_id, orders]

小结

Python 数据结构的选择要看访问方式:按顺序处理用 list,固定组合用 tuple,按 key 查找用 dict,去重和集合运算用 set。写业务代码时,dict + list 的组合最常见,但嵌套过深时要及时抽象成类或 dataclass。