数据持久化存储

前言

  • 从《面向对象(二)》里拆出来的一篇:讲的是把内存里的数据落到磁盘、下次进程还能读回来。
  • 按「通用对象 / 跨语言 / 键值 / 结构化 / 大数组」分五档,每档一个最小可跑示例;所有示例都在本机 Python 3.14.10 上实跑过,输出原样贴在代码块下面。
  • 这些示例会往当前目录写 data.pkl、data.json、mydata、example.db、data.h5,跟着敲之前建议先 cd 到一个临时目录。

五种落盘方式

  • pickle:Python 对象的通用序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import pickle

    data = {'name': 'Alice', 'age': 25, 'scores': [88, 92, 95]}

    with open('data.pkl', 'wb') as f: # 必须二进制模式
    pickle.dump(data, f)

    with open('data.pkl', 'rb') as f:
    loaded = pickle.load(f)

    print(loaded)
    print('默认协议:', pickle.DEFAULT_PROTOCOL, '| 文件大小:', len(pickle.dumps(data)), '字节')
    print('dumps 出来是字节:', pickle.dumps({'a': 1})[:11])

    输出:

    1
    2
    3
    {'name': 'Alice', 'age': 25, 'scores': [88, 92, 95]}
    默认协议: 5 | 文件大小: 58 字节
    dumps 出来是字节: b'\x80\x05\x95\n\x00\x00\x00\x00\x00\x00\x00'

    优点是什么对象都能存;缺点是不安全——pickle.load 反序列化时会执行构造逻辑,来路不明的 .pkl 千万别直接 load。另外它只适合 Python 之间交换,别的语言读不了。

  • json:跨语言、人类可读

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import json

    data = {'name': 'Bob', 'age': 30, 'married': True, 'tags': ('x', 'y')}

    with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, ensure_ascii=False)

    with open('data.json', encoding='utf-8') as f:
    loaded = json.load(f)

    print(open('data.json', encoding='utf-8').read())
    print('读回:', loaded)
    print('tuple 变成了:', type(loaded['tags']).__name__)
    try:
    json.dumps({'s': {1, 2}}) # set 不在 JSON 类型表里
    except TypeError as e:
    print('set 不能序列化:', e)

    输出:

    1
    2
    3
    4
    {"name": "Bob", "age": 30, "married": true, "tags": ["x", "y"]}
    读回: {'name': 'Bob', 'age': 30, 'married': True, 'tags': ['x', 'y']}
    tuple 变成了: list
    set 不能序列化: Object of type set is not JSON serializable

    类型映射是单向的:tuple / set 出去以后回不来原类型(tuple 变 list、set 直接报错)。要存自定义类就得自己写 default= 钩子,或者转成字典再存。

  • shelve:字典手感的持久化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import shelve

    with shelve.open('mydata') as db: # 像字典一样用,落盘由 dbm 负责
    db['name'] = 'Charlie'
    db['info'] = {'age': 35, 'job': 'developer'}

    with shelve.open('mydata') as db:
    print(db['name'], db['info'])
    print('键:', sorted(db.keys()))

    输出:

    1
    2
    Charlie {'age': 35, 'job': 'developer'}
    键: ['info', 'name']

    本质是 pickle + 一个键值索引,值可以是任意可 pickle 对象。Python 3.13 起 dbm 的默认后端换成 sqlite3,所以本机跑完只生成一个 mydata 文件;老版本用的是 dbm 的其他后端,通常会看到 .dir / .dat / .bak 一组文件。

  • sqlite3:结构化数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import os
    import sqlite3

    if os.path.exists('example.db'): # 主键固定,重复插入会撞唯一约束
    os.remove('example.db')

    conn = sqlite3.connect('example.db') # 文件不存在就建,连接要自己关
    c = conn.cursor()
    c.execute('CREATE TABLE IF NOT EXISTS users '
    '(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)')
    c.execute("INSERT INTO users VALUES (1, 'Diana', 28)")
    conn.commit() # 不 commit 就只在这个连接里可见
    print('查询:', list(c.execute('SELECT * FROM users')))

    try:
    c.execute("INSERT INTO users VALUES (1, 'Diana', 28)")
    except sqlite3.IntegrityError as e:
    print('重复主键:', e)

    c.execute("INSERT OR REPLACE INTO users VALUES (1, 'Diana2', 29)")
    conn.commit()
    print('REPLACE 后:', list(c.execute('SELECT * FROM users WHERE id = ?', (1,))))
    conn.close()

    输出:

    1
    2
    3
    查询: [(1, 'Diana', 28)]
    重复主键: UNIQUE constraint failed: users.id
    REPLACE 后: [(1, 'Diana2', 29)]

    查询参数一定要用 ? 占位(上面最后一句那样),字符串拼 SQL 迟早撞上注入。

  • h5py:大型数值数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import h5py
    import numpy as np

    data = np.random.rand(100, 100) # 需要额外安装:pip install h5py
    with h5py.File('data.h5', 'w') as f:
    f.create_dataset('dataset', data=data)

    with h5py.File('data.h5', 'r') as f:
    loaded = f['dataset'][:] # 加 [:] 才是读成 ndarray
    print('形状:', loaded.shape, '| 与原数组相等:', bool((loaded == data).all()))

    输出:

    1
    形状: (100, 100) | 与原数组相等: True

    它只擅长数值数组,字符串、嵌套结构这些不如 JSON / pickle 顺手;f['dataset'] 不加 [:] 拿到的是数据集对象而不是数据。

整体示例:同一个对象四种存法

拿一个带嵌套结构的自定义类当样本,四种方式各存一遍,看谁能原样回来、谁会掉类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import json
import os
import pickle
import shelve
import sqlite3


class Student:
"""一个带嵌套结构的对象,用来对比四种存法的差别"""

def __init__(self, name, scores):
self.name = name
self.scores = scores # 列表里还有字典,考验序列化能力

def __eq__(self, other):
return isinstance(other, Student) and vars(self) == vars(other)

def __repr__(self):
return f'Student({self.name!r}, {self.scores!r})'


stu = Student('小明', [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}])
print('原始对象:', stu)

# 1) pickle:整个对象原样进出
with open('stu.pkl', 'wb') as f:
pickle.dump(stu, f)
with open('stu.pkl', 'rb') as f:
s1 = pickle.load(f)
print('pickle ->', s1, '| 类型:', type(s1).__name__, '| 相等:', s1 == stu)

# 2) json:只能存基本类型,对象要先转 dict,读回来也回不到 Student
with open('stu.json', 'w', encoding='utf-8') as f:
json.dump(vars(stu), f, ensure_ascii=False)
with open('stu.json', encoding='utf-8') as f:
s2 = json.load(f)
print('json ->', s2, '| 类型:', type(s2).__name__)
print('json 想变回对象:', Student(**s2) == stu)

# 3) shelve:键值 + pickle,值可以是任意对象
with shelve.open('stu.db') as db:
db['stu'] = stu
with shelve.open('stu.db') as db:
s3 = db['stu']
print('shelve ->', s3, '| 相等:', s3 == stu)

# 4) sqlite3:得先把对象摊平成列,读回来是元组
os.remove('stu.sqlite') if os.path.exists('stu.sqlite') else None
conn = sqlite3.connect('stu.sqlite')
conn.execute('CREATE TABLE students (name TEXT, scores TEXT)')
conn.execute('INSERT INTO students VALUES (?, ?)', (stu.name, json.dumps(stu.scores, ensure_ascii=False)))
conn.commit()
row = conn.execute('SELECT * FROM students').fetchone()
conn.close()
print('sqlite ->', row, '| 还原 scores:', json.loads(row[1]))

print('落盘文件:', sorted(f for f in os.listdir('.') if f.startswith('stu')))

输出:

1
2
3
4
5
6
7
原始对象: Student('小明', [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}])
pickle -> Student('小明', [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}]) | 类型: Student | 相等: True
json -> {'name': '小明', 'scores': [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}]} | 类型: dict
json 想变回对象: True
shelve -> Student('小明', [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}]) | 相等: True
sqlite -> ('小明', '[{"课": "语文", "分": 92}, {"课": "数学", "分": 85}]') | 还原 scores: [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}]
落盘文件: ['stu.db', 'stu.json', 'stu.pkl', 'stu.sqlite']
  • pickle 和 shelve 能原样还原成 Student 实例(前提是类还能 import 到,否则连读都读不回来)。
  • json 存的是 vars(obj),读回来是 dict;想变回对象得自己 Student(**d) 重建,嵌套对象还要递归处理。
  • sqlite3 是四种里唯一「结构化」的:数据摊平成列之后才能按条件查,代价是对象结构要自己设计表。
  • shelve 在 3.13+ 只生成一个 stu.db 文件(dbm 默认后端换成了 sqlite3),老版本会看到 .dir / .dat / .bak 一组文件。

怎么选

需求 选择 备注
随手存 Python 对象、缓存 pickle 别 load 来路不明的文件
跨语言、要人看、要改 json 类型会退化,注意 ensure_ascii
想要字典手感的键值存储 shelve 底层是 pickle + dbm
有表结构、要查询/事务 sqlite3 单文件数据库,标准库自带
大规模数值数组 h5py 需额外安装,科学计算常用
  • 先想清一件事:数据要不要被别的语言 / 别人读?要 → json;不要,只是自己进程的缓存 → pickle / shelve;要按条件查、要事务 → sqlite3;是巨大的数值数组 → h5py。
  • 再想版本兼容:pickle 的产物和类定义绑得很死,改了类结构再读老文件很容易炸;json 则可以带一个版本号字段平滑迁移。

参考:《面向对象(二)》 里的属性方法、__dict__ 等内容,和这篇的 vars(obj) / self.__dict__ 直接相关。

本文结束 感谢您的阅读
正在加载今日诗词....