🐍 第8章 建立自定义类
📄 一句话概括

JSON 只认五种东西:字符串、数字、布尔、null,以及由它们组成的数组和对象。 datetime 不在里面,所以直接 dumps 会抛 TypeError。 pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是日期。 代价是只有 Python 能读。 pickle.load 在还原对象时会执行类里的构造逻辑。 一个恶意的 .pkl 文件, 可以让你在打开它的瞬间跑起别人的代码。

📝 本章和前面几章的关系

上一篇讲怎么组织数据,这一篇讲怎么把它存成文件。 教材第 8 章后两节是【网络应用程序消息交换格式】和【保存 Python 原生数据】。 前者的主角是 JSON 和 XML,后者是 pickle。三种格式,三套适用场合。 本篇有一处反直觉的数字:pickle 存这一小份数据反而比 JSON 大。 演示里如实摆出来了, 也解释了为什么「省空间」从来不是选 pickle 的理由。

一、同一份数据,三种写法

演示用的数据是一条订单:单号、日期、金额、备注。注意日期是 datetime 对象,不是字符串。 这个细节是整篇的关键。如果日期本来就是字符串,三种格式都不会有问题; 正因为它是真对象,才分出了高下。 四个字段:单号是字符串、日期是 datetime(2026, 11, 2, 9, 30)、金额是 328.5、备注是「含运费」。

真实业务里日期几乎都是对象,因为它要参与计算(差几天、是不是本月)。 存的时候变成字符串,取出来要再解析回对象——这一步的麻烦程度,决定了你选哪种格式。

二、JSON 碰上 datetime 会怎样

json.dumps(数据) 直接抛 TypeError: Object of type datetime is not JSON serializable。 原因很干脆:JSON 标准里只有五种东西——字符串、数字、布尔、null,以及由它们组成的数组和对象。 datetime 不在里面。 最省事的兜底是 json.dumps(d, default=str)。 它碰到不认识的对象就调 str(),于是日期变成 "2026-11-02 09:30:00"。

代价是读回来之后它就是个普通字符串,不再是时间对象。 想再当时间用, 得自己解析(datetime.strptime,第 4.2 篇讲过)。 更讲究的做法是传一个函数进去:碰到 datetime 就转成 ISO 格式字符串,读的时候再转回来。 这样格式统一,别的语言也认。 为什么 JSON 这么严格?因为它要「所有语言都能读」。 要是允许各语言塞自己的对象进去,Python 存的日期 Java 就读不懂了。限制是为了通用。

三、XML 长什么样

它长这样:外层一个 order 标签,里面四个子标签 no、date、amount、note, 每个子标签里夹着值。整串排下来一行放不下,所以演示里是打印出来的。 标签套标签,比 JSON 啰嗦一倍。 同一个 PO-001,JSON 里写 "单号": "PO-001", XML 里要写 <no>PO-001</no>。 但 XML 有两样 JSON 没有的东西:属性和命名空间。 属性写 <no lang="zh">PO-001</no>, 命名空间用来区分「同一个名字在不同系统里的不同意思」。

所以老的系统集成、配置文件、Office 文档内部(.docx 其实就是一堆 XML 打包成 zip), 到今天还在用 XML。 解析用标准库的 xml.etree.ElementTree,几行就能读出来。 注意解析出来的东西永远是字符串,amount 要算数得自己 float()。 这一点跟 CSV 一样:格式本身不带类型信息,类型全靠你解读。

四、pickle:Python 自己的存法

格式 字节数 日期还是时间对象吗 别的语言能读吗
JSON 93 不是,变成字符串了 能
XML 106 不是 能
pickle 123 是 不能

pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是 datetime, 你自己定义的类实例也能原样存。 代价是只有 Python 能读。别的语言打不开,甚至不同 Python 版本之间也可能读不了。 注意这次 pickle 反而比 JSON 大。 93 比 123,小了 30 字节。

为什么?数据这么小的时候,pickle 要额外记类型标记——「这里是个 datetime」这句话本身就要占字节。 数据量上去、结构复杂以后,pickle 才开始占便宜。 但省不省空间从来不是选它的理由,「类型不丢」才是。 你有一堆自定义类的对象、读回来还要接着用, 那就 pickle;只是存个数字表格,用不着它。

五、pickle 的坑:别读来路不明的文件

pickle.load 在还原对象时会执行类里的构造逻辑。 也就是说,一个精心构造的 .pkl 文件, 可以让你在打开它的瞬间跑起别人的代码。 这不是 bug,是设计。pickle 的整个机制就是「按记录重建对象」,重建就得执行代码。 所以规矩很硬: pickle 只用来存自己程序生成的东西。永远不要 load 从网上下载的、或者别人发给你的 .pkl。

这一条没有例外。哪怕对方是你同事,他电脑上也可能已经中招了。 要跨程序、跨语言交换数据,用 JSON。要对人可读、能手改,用 JSON 或 CSV。 要原样保存 Python 的一堆对象、且只在自己机器上用,才轮到 pickle。

六、三种格式怎么选

场景 选什么 原因
给前端接口返回数据 JSON 所有语言都认,浏览器直接能用
给 Excel 或老系统 CSV 双击就开,改起来最省事
对接企业已有系统 XML 对方只认这个,你没得选
存自己程序的中间结果 pickle 类型不丢,读写都快
人要看、还要能手改的配置 JSON 或 TOML 文本格式,改错也看得出来

判断标准只有一条:这份数据以后谁要读。 只有你自己读,挑最省事的;别人要读,挑他能打开的。这一条能解决九成的选择困难。 还有一条隐含的:这份数据要不要活很久? 活很久的用文本格式(JSON、CSV、XML), 因为二十年后的程序还能读它。pickle 不行,Python 版本一换就可能读不了。

本书后面几章都在跟数据库打交道。 到那时候你会发现,上面这些格式处理的是「一小份数据」,数据库处理的是「一大堆还要能查」。 两者的分界线是「要不要按条件筛选」。

七、动手 Demo:三种存法:JSON、XML、pickle

同一份带 datetime 的数据,用三种格式各存一遍。 先看 JSON 直接存会报什么错,再看 pickle 为什么能保住类型。 最后比字节数——注意 pickle 这次反而更大。

import json, pickle, os, tempfile
import xml.etree.ElementTree as ET
from datetime import datetime

d = tempfile.mkdtemp()
data = {'单号': 'PO-001', '日期': datetime(2026, 11, 2, 9, 30),
        '金额': 328.5, '备注': '含运费'}

print('① 同一份数据,三种写法')
print('  数据里的日期是 datetime 对象,不是字符串')
print('  ', data)

print('② JSON 碰上 datetime 会怎样')
try:
    json.dumps(data, ensure_ascii=False)
except TypeError as e:
    print('  json.dumps → TypeError:', e)
js = json.dumps(data, ensure_ascii=False, default=str)
print('  加 default=str 之后:')
print('  ', js)
print('  读回来日期是:', type(json.loads(js)['日期']).__name__)

print('③ XML 长什么样')
root = ET.Element('order')
for k, v in (('no', 'PO-001'), ('date', '2026-11-02T09:30:00'),
             ('amount', '328.5'), ('note', '含运费')):
    ET.SubElement(root, k).text = v
xml_s = ET.tostring(root, encoding='unicode')
print('  ', xml_s)
print('  解析回来:')
for ch in ET.fromstring(xml_s):
    print('    %-8s %s' % (ch.tag, ch.text))
print('  XML 用标签套标签,比 JSON 啰嗦一倍,但能带属性和命名空间。')
print('  解析出来的东西永远是字符串。要数字得自己 float()。')

print('④ pickle:Python 自己的存法')
pkl = pickle.dumps(data)
print('| 格式 | 字节数 | 日期还是时间对象吗 | 别的语言能读吗 |')
print('|---|---:|---:|---|')
print('| JSON | %d | 不是,变成字符串了 | 能 |' % len(js.encode('utf-8')))
print('| XML | %d | 不是 | 能 |' % len(xml_s.encode('utf-8')))
print('| pickle | %d | 是 | 不能 |' % len(pkl))
back = pickle.loads(pkl)
print('  pickle 读回来:type(日期) =', type(back['日期']).__name__)
print('  值 =', back['日期'])
print('  注意这次 pickle 反而比 JSON 大:数据这么小的时候,它要额外记类型标记,亏了。')

print('⑤ pickle 的坑:别读来路不明的文件')
print('  pickle.load 在还原对象时会执行类里的构造逻辑')
print('  也就是说:一个恶意的 .pkl 文件,可以让你在打开它的瞬间跑起别人的代码')
print('  所以规矩很硬:pickle 只用来存自己程序生成的东西,永远不要 load 从网上下载的、')
print('  或者别人发给你的 .pkl。')

print('⑥ 三种格式怎么选')
print('| 场景 | 选什么 | 原因 |')
print('|---|---|---|')
for r in (('给前端接口返回数据', 'JSON', '所有语言都认,浏览器直接能用'),
          ('给 Excel 或老系统', 'CSV', '双击就开,改起来最省事'),
          ('对接企业已有系统', 'XML', '对方只认这个,你没得选'),
          ('存自己程序的中间结果', 'pickle', '类型不丢,读写都快'),
          ('人要看、还要能手改的配置', 'JSON 或 TOML', '文本格式,改错也能看出来')):
    print('| %s | %s | %s |' % r)
print('  判断标准只有一条:这份数据以后谁要读。')

实跑输出:

--------------------------------------------------------------------
三种存法:JSON、XML、pickle
--------------------------------------------------------------------

  ① 同一份数据,三种写法
  --------------------------------------------------------------------
    数据里的日期是 datetime 对象,不是字符串
    {'单号': 'PO-001', '日期': datetime.datetime(2026, 11, 2, 9, 30), '金额': 328.5, '备注': '含运费'}

  ② JSON 碰上 datetime 会怎样
  --------------------------------------------------------------------
    json.dumps → TypeError: Object of type datetime is not JSON serializable
    加 default=str 之后:
    {"单号": "PO-001", "日期": "2026-11-02 09:30:00", "金额": 328.5, "备注": "含运费"}
  JSON 只认五种东西:字符串、数字、布尔、null、以及由它们组成的数组和对象。
  datetime 不在里面,所以得自己转。
  default=str 是最省事的兜底,代价是读回来之后它就是个普通字符串,不再是时间对象——想再当时间用,
  得自己解析。

  ③ XML 长什么样
  --------------------------------------------------------------------
    <order><no>PO-001</no><date>2026-11-02T09:30:00</date><amount>328.5</amount><note>含运费</note></order>
    解析回来:
      no       PO-001
      date     2026-11-02T09:30:00
      amount   328.5
      note     含运费
  XML 用标签套标签,比 JSON 啰嗦一倍,但能带属性和命名空间,老的系统集成、配置文件、
  Office 文档内部都用它。
  解析出来的东西永远是字符串。要数字得自己 float()。

  ④ pickle:Python 自己的存法
  --------------------------------------------------------------------

| 格式 | 字节数 | 日期还是时间对象吗 | 别的语言能读吗 |
|---|---:|---:|---|
| JSON | 93 | 不是,变成字符串了 | 能 |
| XML | 106 | 不是 | 能 |
| pickle | 123 | 是 | 不能 |

    pickle 读回来:type(日期) = datetime
    值 = 2026-11-02 09:30:00
  pickle 存的是「这个对象在 Python 里长什么样」,所以日期读回来还是日期,
  你自己定义的类实例也能原样存。代价是只有 Python 能读,别的语言打不开。
  注意这次 pickle 反而比 JSON 大:数据这么小的时候,它要额外记类型标记,亏了。
  数据量上去、结构复杂以后,pickle 才开始占便宜——但省不省空间从来不是选它的理由,
  「类型不丢」才是。

  ⑤ pickle 的坑:别读来路不明的文件
  --------------------------------------------------------------------
    pickle.load 在还原对象时会执行类里的构造逻辑
    也就是说:一个恶意的 .pkl 文件,可以让你在打开它的瞬间跑起别人的代码
  所以规矩很硬:pickle 只用来存自己程序生成的东西,永远不要 load 从网上下载的、
  或者别人发给你的 .pkl。
  要跨程序、跨语言交换数据,用 JSON。要对人可读、能手改,用 JSON 或 CSV。
  要原样保存 Python 的一堆对象、且只在自己机器上用,才轮到 pickle。

  ⑥ 三种格式怎么选
  --------------------------------------------------------------------

| 场景 | 选什么 | 原因 |
|---|---|---|
| 给前端接口返回数据 | JSON | 所有语言都认,浏览器直接能用 |
| 给 Excel 或老系统 | CSV | 双击就开,改起来最省事 |
| 对接企业已有系统 | XML | 对方只认这个,你没得选 |
| 存自己程序的中间结果 | pickle | 类型不丢,读写都快 |
| 人要看、还要能手改的配置 | JSON 或 TOML | 文本格式,改错也能看出来 |

  判断标准只有一条:这份数据以后谁要读。只有你自己读,挑最省事的;别人要读,挑他能打开的。

第一段那个 datetime 对象是整篇的关键。 日期要是本来就是字符串,三种格式都不会出问题,也就分不出高下了。 第二段那个 TypeError 值得读原文:not JSON serializable,意思是「不能序列化成 JSON」。 default=str 是最省事的兜底,代价是读回来就只是字符串了。 第三段 XML 那串要横着看:一对标签包一个值。比 JSON 啰嗦,但它能带属性,老系统就认这个。 第四段那个 123 比 93 要看清:pickle 存这一小份反而更大。 因为它要额外记「这里是个 datetime」,这句说明本身就要占字节。 省不省空间从来不是选 pickle 的理由,类型不丢才是。 第五段那条规矩没有例外。 pickle 的机制就是「按记录重建对象」,重建就得执行代码,所以恶意文件能在你打开它的瞬间跑起来。

30 秒实验

  1. 把演示第一段的 datetime(2026, 11, 2, 9, 30) 换成字符串 '2026-11-02',重跑, 看 JSON 还会不会报错,pickle 的字节数变多少。
  2. 把演示第二段的 default=str 去掉,改成自己写一个转换函数, 让日期输出成 ISO 格式(isoformat())。
  3. 把演示第四段的数据里加一个列表(比如两个商品明细),再看三种格式的字节数差多少。

常见误区

  1. 「JSON 能存任何 Python 对象。」只认五种东西:字符串、数字、布尔、null,以及它们的组合。 日期、集合、自定义类都不行,得自己转。
  2. 「pickle 比 JSON 小,所以存数据该用 pickle。」演示里它反而更大。 省不省空间从来不是选它的理由,「类型不丢」才是。 而且它只有 Python 能读。
  3. 「pickle 读个文件而已,能有什么风险。 」pickle.load 会执行代码。 来路不明的 .pkl 等于一个可执行文件,绝不能打开。
自测三问(点开对答案) 1. `json.dumps` 碰到 `datetime` 为什么报错?怎么办? —— 因为 JSON 标准只认五种东西:字符串、数字、布尔、null 和它们的组合,没有日期类型。 最简单的办法是 `default=str` 把它转成字符串; 讲究一点就传个函数转成 ISO 格式,读的时候再 `strptime` 解析回来。 2. pickle 为什么能保住 `datetime` 类型,JSON 不行? —— 因为 pickle 存的是「这个对象在 Python 里长什么样」,包括它的类型,读的时候按记录重建。 JSON 是给所有语言用的通用格式,只能存那五种基本类型,类型信息就丢了。 代价是 pickle 只有 Python 能读。 3. 为什么绝对不能 load 别人发来的 `.pkl` 文件? —— 因为 `pickle.load` 在还原对象时会执行类里的构造逻辑。 一个精心构造的 `.pkl` 可以让你的程序在打开它的瞬间跑起别人的代码。 这不是漏洞,是 pickle 的机制决定的——它必须执行代码才能重建对象。

第 8 章结束。下一篇进第 9 章:用 Flask 起一个真能访问的网站。

相关笔记