(八)JSON 与 Python 对象之间的转换
1. 字符串转换与文件读写
一本书现在有标题、完成状态和标签,仅靠一行一个书名已经无法完整保存。JSON 可以把映射、列表和基本值组织成一份文本:
# dumps 把对象编码为 JSON 字符串,loads 从字符串还原基本数据。
import json
record = {"title": "Python 入门", "finished": False, "tags": ["基础"]}
text = json.dumps(record, ensure_ascii=False, indent=2)
restored = json.loads(text)
print(restored["title"], restored["finished"], restored["tags"])
print(type(text).__name__, type(restored).__name__)得到 Python 入门 False ['基础'] 和 str dict。ensure_ascii=False 让中文直接保留在文本中,indent=2 给嵌套内容加入两格缩进。JSON 文本使用 false,回到 Python 后是 False。
| Python 值 | JSON 表示 | 读取后的 Python 值 |
|---|---|---|
| 字符串 | 字符串 | str |
| 整数、浮点数 | 数字 | int、float |
True、False |
true、false |
bool |
None |
null |
None |
| 列表、元组 | 数组 | list |
| 字符串键字典 | 对象 | dict |
JSON 对象的键是字符串,整数键转成文本再读回来后,不再是原来的整数键。元组也会读成列表,所以要按约定的记录结构还原,而不是把 JSON 当作任意 Python 对象的原样复制。
# dump/load 接收文件对象,与 dumps/loads 接收字符串的接口分开。
import json
from pathlib import Path
path = Path("books.json")
with path.open("w", encoding="utf-8") as stream:
json.dump([{"title": "Python", "finished": True}], stream, ensure_ascii=False)
with path.open(encoding="utf-8") as stream:
records = json.load(stream)
print(records)输出 [{'title': 'Python', 'finished': True}]。一次文件保存一份 JSON 文档;连续往末尾追加两个独立对象,会变成两份粘在一起的文档,json.load 会报告额外内容。日志式逐条保存可以采用每行一份 JSON 的 JSON Lines 格式,再逐行解析。JSON 接口
2. 日期、精确小数与缺失值
date、Decimal、自定义 Book 实例并不是 JSON 自带的数据类型。先明确它们保存成什么,再在读取后按同一规则恢复:
# 日期保存为 ISO 字符串,Decimal 保存十进制文本,读取后明确恢复类型。
from datetime import date
from decimal import Decimal
import json
record = {
"added_on": date(2026, 10, 19).isoformat(),
"price": str(Decimal("19.90")),
"note": None,
}
text = json.dumps(record, ensure_ascii=False, allow_nan=False)
restored = json.loads(text)
added_on = date.fromisoformat(restored["added_on"])
price = Decimal(restored["price"])
print(added_on.isoformat(), price + Decimal("0.10"), restored["note"])得到 2026-10-19 20.00 None。把所有未知对象一律转换成字符串,会丢掉恢复所需的类型含义;这里在字段约定中明确保存方式。allow_nan=False 还会拒绝把非有限浮点数写成 JSON 的 NaN 或 Infinity。需要直接把 JSON 小数读成 Decimal 时,可以使用 json.loads(text, parse_float=Decimal)。
# 没有字段、字段为 null、字段为 0,分别表示不同记录状态。
records = [{}, {"minutes": None}, {"minutes": 0}]
for record in records:
if "minutes" not in record:
print("没有分钟数字段")
elif record["minutes"] is None:
print("分钟数尚未填写")
else:
print("分钟数", record["minutes"])输出“没有分钟数字段”“分钟数尚未填写”“分钟数 0”。只写 if not record.get("minutes") 会把这三种情况都归为假值,读记录时应按它们各自的含义处理。
3. 语法解析与记录校验
json.loads 成功只说明文本是 JSON。{"finished": "false"} 仍然是合法 JSON,但里面保存的是非空字符串;执行 bool("false") 会得到 True。
# 解析与字段校验分两步,显式拒绝把文本 false 当成布尔状态。
import json
def parse_finished(text):
record = json.loads(text)
value = record["finished"]
if type(value) is not bool:
raise ValueError("finished 应是布尔值")
return value
print(parse_finished('{"finished": false}'))
try:
parse_finished('{"finished": "false"}')
except ValueError as error:
print(error)输出 False 和“finished 应是布尔值”。综合示例会把这一步整理为记录转换函数,先核对全部待导入数据,再交给存储层。