列表按下标取,第三位是啥得自己记。字典按键取,rec['品名'] 一眼就知道拿的是什么。
方括号取不到会抛 KeyError,get 取不到给 None 或你给的默认值。
字段可能缺的时候一律用 get。
真实数据几乎都是「字典装字段,里面再套字典或列表」。JSON、接口返回、配置文件,全是这个形状。
前六章抓到的数据都散在变量里,程序一关就没了。这一章开始存。 教材第 7 章开头两节是【使用字典保存结构化数据】和【在本机电脑保存数据】。 顺序是:先定「一条记录长什么样」(字典),再定「一批记录怎么落盘」(JSON、CSV)。 本篇末尾那个 8500 对 4542 是实测的,它说明一件事:同一样数据,写法不同体积差近一倍。
一、列表的麻烦:第三位是啥
用列表存一条货:['六角螺栓 M8', 120, 1.5]。
取第三个写 x[2]——一个月后你自己都不记得 2 是单价还是数量。
字典解决的就是这件事:{'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5},
取的时候写 rec['单价']。
这一行代码自己说明了意图。 读代码的人不用回头翻「当初第几位放的是什么」。
字典的键必须是不可变类型(第 4.3 篇那张表),所以字符串、数字、元组能当键,列表不行。 实际用起来几乎全是字符串。
二、按键取,还是用 get
rec['规格'] 取一个不存在的键,直接抛 KeyError: '规格',退出码非 0。
rec.get('规格') 取不到给 None;rec.get('规格', '未填') 取不到给你指定的默认值。
字段可能缺的时候一律用 get,别赌它一定在。 尤其是数据来自别人、来自网页、
来自用户输入的时候。
什么时候可以用方括号?你自己刚写进去的键,确定在。 那种情况用方括号更直接,而且真缺了就该炸——说明你的逻辑错了。 这两种行为的差别,跟第 3.1 篇列表「取值越界报错、切片越界不报错」是一个道理: 「我就要这个」和「有就给我」是两种语义,得用两种写法。
三、加、改、删
给一个不存在的键赋值就是加,给已有的键赋值就是改。字典不区分这两种动作,看键在不在。
演示里加「规格」和改「数量」用的是同一句 rec['规格'] = ...。
所以「我不小心把已有字段覆盖了」这种 bug 不会报错,只会悄悄发生。
删除用 pop(键),它把值还给你;也有 del rec[键],那个不给返回值。
判断某栏在不在,用 '单价' in rec,别用 try。
in 判断的是键,不是值。'六角螺栓 M8' in rec 是 False——哪怕它确实是某个键的值。
要查值得写 '六角螺栓 M8' in rec.values()。
四、走一遍字典
for k in rec 拿到的是键,不是键值对。这一点跟列表不一样,第一次用容易愣住。
| 写法 | 给你什么 |
|---|---|
for k in rec |
键(不是键值对) |
for k, v in rec.items() |
键和值 |
rec.keys() |
所有键 |
rec.values() |
所有值 |
顺序是插入的顺序。从 Python 3.7 起这一点是有保证的,可以依赖。
要按键排序走一遍,先 sorted(rec)。
要按值排,sorted(rec.items(), key=lambda kv: kv[1])——就是第 5.3 篇那个 key 的用法。
五、一层套一层:一条订单
一条真订单长这样:单号、客户(名称、联系人)、明细(好几行,每行是品名、数量、单价)。
它不是「一条记录」,是「一条记录里套着别的记录」。 字典装字段,字段的值可以是另一个字典,
也可以是列表。
演示第四段打出来:单号 PO-20261102-001、客户名、第一条明细、明细条数 2、合计数量 200。
真实数据几乎都是这个形状。
JSON、HTTP 接口的返回、配置文件、第 8 章要讲的 XML,全是「字典套列表套字典」。
理解了这个形状,后面很多事就顺了:「取深层的一个值」就是把下标和键一层层串起来,
order['客户']['名称']、order['明细'][0]['品名']。
六、存成文件再读回来
json.dumps(对象) 把字典变成字符串,json.loads(字符串) 变回来。
带 s 的两个处理字符串,不带 s 的 dump/load 直接处理文件。
演示里写出去 266 字节,读回来单号对得上、明细条数还是 2。这一步是「程序关掉数据还在」的开始。
有两个参数必须知道。
ensure_ascii=False——不加它,中文会变成 \u4e2d 这样的转义,文件能读但人看不懂。
indent=2 是给人看的排版,一行一层缩进,文件会大一些。
给机器读的接口数据不用 indent,省下来的都是带宽。
还有一个坑:json.dumps 碰到 datetime 对象会抛 TypeError,因为 JSON 标准里没有日期类型。
要么先 str() 转成字符串,要么传 default=str 让它自己转。第 8 章会细讲。
七、一百条记录,键名重复了几遍
| 存法 | 字节数 | 每条多少 |
|---|---|---|
| 每行一个字典 | 8500 | 85.0 |
| 表头 + 一行一个数组 | 4542 | 45.4 |
差 3958 字节,全是重复的键名。 一百条记录,同一批键名被写了 100 遍。 这就是列式存储的思路:同一列的键只存一次。 数据量小的时候不用管,上万条之后这个差别很可观。 代价是可读性。表头加数组那种存法,少一行就整列错位,人也看不出第三列是数量还是单价。 自己程序内部传可以这么压,给别人看别这么干。
顺带一提:这种「表头只写一次」的形状,就是下一篇 CSV 的形状。CSV 之所以比 JSON 小,原因就在这里。
八、动手 Demo:字典:一条记录该长什么样
先看按键取和 get 的差别,再走一遍增删改。 然后套一层,做一条带明细的真订单。 最后把同一批数据用两种写法存一遍,比字节数。
import json
print('① 键值对,按键取')
rec = {'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5}
print(' rec =', rec)
print(" rec['品名'] =", rec['品名'])
print(" rec.get('规格') =", rec.get('规格'))
print(" rec.get('规格', '未填') =", rec.get('规格', '未填'))
try:
rec['规格']
except KeyError as e:
print(' rec[\'规格\'] → KeyError:', e)
print('② 加、改、删')
rec['规格'] = 'M8×40'
print(' 加一栏 ', rec)
rec['数量'] = 100
print(' 改数量 ', rec)
got = rec.pop('规格')
print(' pop 掉规格 拿走 %s,剩 %s' % (got, rec))
print(" in 判断 '单价' in r → %s" % ('单价' in rec))
print(" 查值要写 values():'六角螺栓 M8' in rec.values() → %s"
% ('六角螺栓 M8' in rec.values()))
print('③ 走一遍字典')
print(' for k in rec →', ' '.join(rec))
print(' for k, v in .items() →', ';'.join('%s=%s' % kv for kv in rec.items()))
print(' .keys() ', list(rec.keys()))
print(' .values()', list(rec.values()))
print('④ 一层套一层:一条订单')
order = {'单号': 'PO-20261102-001',
'客户': {'名称': '某装饰公司', '联系人': '王工'},
'明细': [{'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5},
{'品名': '膨胀螺栓 10mm', '数量': 80, '单价': 2.3}]}
print(' 单号 ', order['单号'])
print(' 客户名 ', order['客户']['名称'])
print(' 第一条 ', '%s × %d' % (order['明细'][0]['品名'], order['明细'][0]['数量']))
print(' 明细条数', len(order['明细']))
print(' 合计数量', sum(d['数量'] for d in order['明细']))
print('⑤ 存成文件再读回来')
s = json.dumps(order, ensure_ascii=False, indent=2)
open('order.json', 'w', encoding='utf-8').write(s)
print(' 写出去 %d 字节' % len(s.encode('utf-8')))
for ln in s.splitlines()[:6]:
print(' ' + ln)
back = json.loads(open('order.json', encoding='utf-8').read())
print(' 读回来单号对得上?', back['单号'] == order['单号'])
print(' 明细条数', len(back['明细']))
print(' 不加 ensure_ascii 会变成:', json.dumps({'品名': '螺栓'})[:40])
print('⑥ 一百条记录,键名重复了几遍')
rows = [{'日期': '2026-11-%02d' % (i % 3 + 1), '品名': '品名%d' % (i % 7),
'数量': i % 50 + 1, '单价': 1.5 + i % 9} for i in range(100)]
a = json.dumps(rows, ensure_ascii=False).encode('utf-8')
head = ['日期', '品名', '数量', '单价']
b = (json.dumps(head, ensure_ascii=False)[:-1] + ',"rows":'
+ json.dumps([[r[h] for h in head] for r in rows], ensure_ascii=False) + '}')
b = b.encode('utf-8')
print('| 存法 | 字节数 | 每条多少 |')
print('|---|---:|---:|')
print('| 每行一个字典 | %d | %.1f |' % (len(a), len(a) / 100.0))
print('| 表头 + 一行一个数组 | %d | %.1f |' % (len(b), len(b) / 100.0))
print(' 100 条记录,同一批键名被写了两遍以上,多出来的 %d 字节全是它们。' % (len(a) - len(b)))
实跑输出:
--------------------------------------------------------------------
字典:一条记录该长什么样
--------------------------------------------------------------------
① 键值对,按键取
--------------------------------------------------------------------
rec = {'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5}
rec['品名'] = 六角螺栓 M8
rec.get('规格') = None
rec.get('规格', '未填') = 未填
rec['规格'] → KeyError: '规格'
方括号取不到会抛 KeyError,get 取不到给 None 或你指定的默认值。
字段可能缺的时候一律用 get,别赌它一定在。
② 加、改、删
--------------------------------------------------------------------
加一栏 {'品名': '六角螺栓 M8', '数量': 120, '单价': 1.5, '规格': 'M8×40'}
改数量 {'品名': '六角螺栓 M8', '数量': 100, '单价': 1.5, '规格': 'M8×40'}
pop 掉规格 拿走 M8×40,剩 {'品名': '六角螺栓 M8', '数量': 100, '单价': 1.5}
in 判断 '单价' in r → True
字典的键不能重复。给已有的键赋值就是改,不是加。要判断某栏在不在,用 in,别用 try。
③ 走一遍字典
--------------------------------------------------------------------
for k in rec → 品名 数量 单价
for k, v in .items() → 品名=六角螺栓 M8;数量=120;单价=1.5
.keys() ['品名', '数量', '单价']
.values() ['六角螺栓 M8', 120, 1.5]
直接 for 拿到的是键。
要键值一起拿写 .items(),顺序是插入的顺序,从 Python 3.7 起这一点是有保证的。
④ 一层套一层:一条订单
--------------------------------------------------------------------
单号 PO-20261102-001
客户名 某装饰公司
第一条 六角螺栓 M8 × 120
明细条数 2
合计数量 200
真实数据几乎都是这个形状:字典装一层的字段,里面再套字典或列表。
JSON、接口返回、配置文件,全是这个结构。
⑤ 存成文件再读回来
--------------------------------------------------------------------
写出去 266 字节
{
"单号": "PO-20261102-001",
"客户": {
"名称": "某装饰公司",
"联系人": "王工"
},
"明细": [
{
读回来单号对得上?True
明细条数 2
json.dumps 把字典变成字符串,json.loads 变回来。
ensure_ascii=False 才能让中文原样写出来,不然会变成一堆 \u4e2d 这样的转义。
indent=2 是给人看的排版,文件会大一些。给机器读的接口数据不用 indent,省下来的都是带宽。
⑥ 一百条记录,键名重复了几遍
--------------------------------------------------------------------
| 存法 | 字节数 | 每条多少 |
|---|---:|---:|
| 每行一个字典 | 8500 | 85.0 |
| 表头 + 一行一个数组 | 4542 | 45.4 |
100 条记录,同一批键名被写了两遍以上,多出来的 3958 字节全是它们。
这就是列式存储的思路:同一列的键只存一次。数据量小的时候不用管,上万条之后这个差别就很可观了。
代价是可读性:表头 + 数组那种存法,少一行就错位,人也看不出第三列是数量还是单价。
自己程序内部传可以这么压,给别人看别这么干。
第一段那个 KeyError 和 get 的对照是本篇最实用的一处。字段可能缺的时候一律用 get,别赌它一定在。 第二段有个容易忽略的地方:给已有键赋值就是改,不报错。 所以「不小心覆盖了已有字段」这种 bug 是静默的。 第三段那句 直接 for 拿到的是键 要记牢。要键值对写 .items(),顺序就是插入顺序,3.7 以后有保证。 第四段那个 order 三层嵌套:字典里套字典、字典里套列表、列表里再套字典。真实数据几乎都是这个形状。 第五段末尾那行对照很直观:不加 ensure_ascii,中文全变成转义序列,文件照样能读,只是人看不懂。 第六段那一千多字节的差距全是重复的键名。这就是 CSV 比 JSON 小的原因:表头只写一次。
30 秒实验
- 把演示第一段的
rec.get('规格', '未填')改成rec.get('规格', 0),看返回什么类型。 - 把演示第四段的
sum(d['数量'] for d in order['明细'])改成算总金额(数量乘单价再求和), 看结果是 364.0 还是别的。 - 把演示第六段的 100 改成 1000,重跑,看两种存法的倍数关系变没变。
常见误区
- 「字典取值都用方括号。」取不到的键会抛
KeyError。 数据来自别人时一律用get,别赌它一定在。 - 「
'值' in 字典能查值。」它查的是键。要查值写'值' in d.values()。 这个错不报错,只是永远返回 False。 - 「
json.dumps直接就能存中文。」不加ensure_ascii=False,中文会变成\u4e2d这样的转义。 文件能读,但人看不懂,也没法直接搜。
自测三问(点开对答案)
1. `rec['规格']` 和 `rec.get('规格')` 有什么区别?什么时候该用哪个? —— 前者取不到抛 `KeyError`,后者取不到给 `None`(或你指定的默认值)。 数据来源不确定、字段可能缺的时候用 `get`; 自己刚写进去、确定在的键可以用方括号——真缺了就该炸,说明逻辑错了。 2. `for k in rec` 拿到的是什么?要键值一起拿怎么写? —— 拿到的是键,不是键值对。要一起拿写 `for k, v in rec.items()`。 这个默认值跟列表不一样,第一次用字典很容易愣住。 3. 同样一百条记录,「每行一个字典」8500 字节,「表头加数组」4542 字节。差的那四千字节是什么? —— 是重复的键名。 一百条记录把「日期」「品名」「数量」「单价」各写了 100 遍,而表头那种存法只写一遍。 这就是列式存储的思路,也是 CSV 比 JSON 小的原因。下一篇把这种「表头只写一次」的形状落到文件里:CSV。顺便说说 Excel 打开乱码是怎么回事。
- ☐ (本篇完,下一篇 7.2 CSV 与 Google 试算表)