数据持久化存储
前言
- 从《面向对象(二)》里拆出来的一篇:讲的是把内存里的数据落到磁盘、下次进程还能读回来。
- 按「通用对象 / 跨语言 / 键值 / 结构化 / 大数组」分五档,每档一个最小可跑示例;所有示例都在本机 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
13import 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
17import 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
9import 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
2Charlie {'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
23import 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
10import 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 | import json |
输出:
1 | 原始对象: Student('小明', [{'课': '语文', '分': 92}, {'课': '数学', '分': 85}]) |
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__ 直接相关。