Python 现代开发学习笔记

(八)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 应是布尔值”。综合示例会把这一步整理为记录转换函数,先核对全部待导入数据,再交给存储层。

笔记正文与全部题解 · Ctrl / ⌘ K