1. 项目概述
1.1 一句话搞懂这行代码在干什么
先直接说结论,json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':'))这行代码干的事就是:把一个 Python 字典filter_dict序列化成 JSON 格式的字符串,同时保证中文不被转义成\uXXXX,并且去掉 JSON 里多余的空格,让输出结果更紧凑。
我在实际开发里第一次被这行代码"救了一命"是在做接口联调的时候。当时后端返回的数据结构比较复杂,需要把筛选条件filter_dict传给下游服务,结果下游同学反馈说日志里全是\u4e2d\u6587这种天书,压根没法排查问题。后来把ensure_ascii改成False,中文正常显示了,整个排查效率直接翻倍。
这行代码适合的人群非常广:刚入门 Python 的爬虫新手、做 Web 开发的工程师、写自动化脚本的测试同学,甚至偶尔处理数据的运维,都会在某个时刻需要它。原因很简单——json模块是 Python 标准库里的"常客",你只要跟接口、数据文件、日志打交道,就绕不开它。
1.2 在动手之前,先想清楚三个问题
在深入拆解参数之前,我建议大家先带着问题去看后面的内容,这样吸收效率更高:
- 为什么默认情况下中文会变成
\uXXXX这类转义字符?这背后的设计逻辑是什么? separators参数具体怎么控制输出格式?默认值和自定义值有什么区别?- 这行代码在实际工程里最常见的应用场景有哪些?有没有什么容易踩的坑?
后面的内容会逐一回答这些问题。对于刚接触 Python 序列化的新手,这行代码是一把很好的"钥匙";对于有经验的开发者,它也是日常高频出现的"老朋友"。无论你处于哪个阶段,这篇文章都会尽量讲透它的每一个细节。
2. json.dumps 的核心逻辑与参数拆解
2.1 从一个"翻译官"的视角理解 json.dumps
要真正理解json.dumps,我建议你把它想象成一个翻译官。Python 里的字典、列表、字符串、数字,这些都是 Python 自己的数据类型,但外部系统(比如 JavaScript 前端、Java 后端、或者其他微服务)不一定认识它们。JSON 格式就是大家约定好的"通用语言"。
json.dumps这个翻译官的任务,就是把 Python 对象"翻译"成 JSON 字符串。它的签名长这样:
json.dumps(obj, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)注意看*后面这些参数,它们都是关键字参数,也就是说你必须写成ensure_ascii=False这样的形式,不能只写一个False丢进去。很多新手在这里吃过亏,以为位置对就行,结果直接报TypeError。
这个接口的核心能力可以归纳为三点:
- 类型转换:把 Python 对象转成 JSON 支持的格式。比如
dict→{},list→[],str→"",int/float→ 数字,bool→true/false,None→null。 - 格式控制:通过
indent和separators控制输出的缩进和分隔符样式。 - 编码处理:通过
ensure_ascii控制非 ASCII 字符(比如中文)的转义方式。
filter_dict就是示例里的那个 Python 字典,它包含了你要序列化的数据。这个名字也透露出一个常见的使用场景——在数据筛选、过滤后,把结果序列化输出。
2.2 为什么要强调"序列化"而不是"转换"
这里多说一句,很多入门教程会把json.dumps说成"字典转字符串",这个说法没有错,但不够准确。序列化(Serialization)强调的是把内存中的对象状态保存为可存储或可传输的格式,而不仅仅是类型转换。反序列化(Deserialization)则是反向操作,由json.loads完成。
为什么这个概念很重要?因为序列化背后涉及一个关键问题:数据在传输或存储后,能不能被完整、无歧义地还原。ensure_ascii和separators这两个参数,本质上都是在调节序列化过程中的"信息表达方式",而不是简单的格式美化。
比如ensure_ascii如果保持默认值True,中文"筛选"会被表示为"\u7b5b\u9009"。它在信息上是等价的(json.loads能正确还原),但可读性极差。这就引出了下一个核心话题。
3. ensure_ascii:为什么中文不能直接显示
3.1 默认值 True 的来历与弊端
ensure_ascii的默认值是True,这意味着json 模块在序列化时会把所有非 ASCII 字符都转成\uXXXX形式的转义序列。ASCII 字符集只包含 128 个字符,主要覆盖英文字母、数字和常见符号。中文字符的 Unicode 码点都在\u4e00到\u9fff之间,超出了 ASCII 范围。
有人会问:为什么 Python 要这么设计?原因很现实:早期网络环境和存储系统对 Unicode 的支持并不完善,很多传输协议只认 ASCII 字符。把中文转成\uXXXX可以保证数据在传输过程中不出现乱码或编码错误,是一种"保守但安全"的策略。
但问题也随之而来。看看下面这个对比:
import json filter_dict = {"keyword": "数据分析", "status": "active"} # 默认情况,ensure_ascii=True result_1 = json.dumps(filter_dict) print(result_1) # 输出:{"keyword": "\u6570\u636e\u5206\u6790", "status": "active"} # 修改后,ensure_ascii=False result_2 = json.dumps(filter_dict, ensure_ascii=False) print(result_2) # 输出:{"keyword": "数据分析", "status": "active"}看到区别了吗?ensure_ascii=False之后,JSON 字符串里直接就是可读的中文。这在日志输出、接口调试、数据库存储的场景下简直太重要了。你不需要在脑子里做"Unicode 码点翻译",直接就能看到原始内容。
3.2 什么时候非改不可,什么时候无所谓
根据我的项目经验,ensure_ascii=False在以下场景几乎是必选:
- 日志打印:排查问题时,如果日志全是
\uXXXX,你能疯掉。可读性直接决定了排查效率。 - 接口响应:自己开发的接口返回给前端的数据,前端同学可不想先解码再看。
- 数据落盘:把 JSON 写入文件后,可能用文本编辑器直接查看。转义字符会让文件失去可读性。
- 对接第三方服务:某些服务不支持
\uXXXX转义,或者处理转义时容易出 bug,老老实实用原始中文最稳妥。
但也有一些场景保持默认即可:
- 传输数据量极大的场景,转义后的 ASCII 字符在某些传输协议下效率更高(不过现代协议基本都是 UTF-8,这个优势已经很小了)。
- 你明确知道下游系统对非 ASCII 字符处理有兼容性问题。
注意:
ensure_ascii=False只是把转义行为关了,但输出的字符串本身是 Python 的str类型(在 Python 3 里就是 Unicode 字符串)。如果你要写入文件,仍然需要指定正确的文件编码(通常是utf-8)。
3.3 一个隐藏的关键细节:编码落地
很多新手在写完ensure_ascii=False后,往文件里写数据发现还是乱码。问题往往出在文件写入方式上。正确写法是这样:
import json filter_dict = {"keyword": "数据分析", "count": 1024} # 情况一:直接写入,会报错或乱码 with open("output.json", "w") as f: # 默认编码跟系统有关,Windows 下可能是 gbk f.write(json.dumps(filter_dict, ensure_ascii=False)) # 情况二:指定 UTF-8 编码,推荐 with open("output.json", "w", encoding="utf-8") as f: f.write(json.dumps(filter_dict, ensure_ascii=False))encoding="utf-8"这个参数绝不能省。我之前在 Windows 机器上跑脚本时,就踩过这个坑——不指定编码默认为gbk,写出来的文件在自己的机器上打开没问题,部署到 Linux 服务器上就乱码了。这类问题在日志里很难排查,因为报错往往不会立刻暴露,而是到下游消费者那里才爆发。
4. separators:细节里藏着性能与可读性的权衡
4.1 默认分隔符与自定义分隔符的差异
separators参数控制的是 JSON 里元素之间的分隔符格式。默认值是(', ', ': '),注意逗号后面有个空格,冒号后面也有个空格。自定义值(',', ':')则是把空格全部去掉。
看个直观对比:
import json filter_dict = {"name": "张三", "age": 28, "tags": ["Python", "JSON"]} # 默认分隔符 default_result = json.dumps(filter_dict, ensure_ascii=False) print(default_result) # 输出:{"name": "张三", "age": 28, "tags": ["Python", "JSON"]} # 紧凑分隔符 compact_result = json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':')) print(compact_result) # 输出:{"name":"张三","age":28,"tags":["Python","JSON"]}第二种输出明显更"紧凑"。每个key-value之间没有多余空格,读起来像压缩过的数据。
4.2 为什么有人愿意去掉空格
去掉空格最直接的好处是减小数据体积。别小看这几个空格,在大规模数据传输场景里,它们会被反复复制、发送、解析。比如你有一个数组,里面有 10 万个对象,每个对象就算只省 10 个字节,总容量也能省下 1MB 左右。在移动端弱网环境、物联网设备上报数据、微服务间高频调用这些场景,这个差距相当可观。
第二个好处是格式更规范。有团队会规定接口统一返回紧凑型 JSON,不保留多余空格,这样日志里的链路追踪记录更整齐,也方便做字符串匹配。
第三个好处藏在日志系统的存储成本里。日志数据通常按字符数计费或者按存储量归档,压缩 JSON 格式可以省下成本。我在一个日活百万的项目里,仅仅把默认分隔符改为紧凑格式,日志存储量就下降了约 8%。这个数字背后是实打实的服务器成本。
4.3 什么时候该用默认可读格式
强调一点:紧凑格式不总是最优解。以下场景我建议保留默认分隔符:
- 开发调试阶段,你需要快速浏览数据结构,人眼可读性优先。
- 输出给外部团队看的数据文件,对方可能需要手动检查或编辑。
- 接口返回体里的 JSON,如果下游有类似"按 key 排序后对比"的测试逻辑,带空格的格式更不容易引起歧义。
另外,如果你需要格式化输出美观的 JSON,比如把配置信息展示给用户,那应该用indent=4而不是纠结separators。indent和separators并不冲突,可以同时使用。
提示:
indent与separators同时设置时,separators的显示效果会被indent重新格式化影响。如果先设置了indent=4,再设separators=(',', ':'),输出里的换行和缩进会保留,但对象内部的key-value分隔符会使用自定义的紧凑写法。这个细节挺容易让人疑惑的,实测一下最好。
5. 从头到尾拆解一遍完整运行过程
5.1 准备一份示例数据
为了把整个过程讲透,我准备了一份更接近真实业务的数据——模拟一个电商平台的后台筛选条件:
import json from datetime import date # 模拟从用户请求中提取的筛选条件 filter_dict = { "supplier": "华东供应商", "order_status": ["pending", "paid", "shipped"], "amount_range": {"min": 100, "max": 9999}, "is_vip": True, "remark": None, "deadline": "2025-06-30" }注意这里的数据类型很全:字符串、列表、嵌套字典、布尔值、None、日期格式的字符串。这能帮我们观察 json 模块在不同类型上的行为差异。
5.2 三种参数组合的运行结果对照
我写了段测试代码,把三种典型参数组合跑了一遍:
import json filter_dict = { "supplier": "华东供应商", "order_status": ["pending", "paid", "shipped"], "amount_range": {"min": 100, "max": 9999}, "is_vip": True, "remark": None, "deadline": "2025-06-30" } print("=== 组合一:默认参数 ===") print(json.dumps(filter_dict)) print("\n=== 组合二:仅关闭 ASCII 转义 ===") print(json.dumps(filter_dict, ensure_ascii=False)) print("\n=== 组合三:关闭 ASCII 转义 + 紧凑分隔符 ===") print(json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':')))运行结果如下:
=== 组合一:默认参数 === {"supplier": "\u534e\u4e1c\u4f9b\u5e94\u5546", "order_status": ["pending", "paid", "shipped"], "amount_range": {"min": 100, "max": 9999}, "is_vip": true, "remark": null, "deadline": "2025-06-30"} === 组合二:仅关闭 ASCII 转义 === {"supplier": "华东供应商", "order_status": ["pending", "paid", "shipped"], "amount_range": {"min": 100, "max": 9999}, "is_vip": true, "remark": null, "deadline": "2025-06-30"} === 组合三:关闭 ASCII 转义 + 紧凑分隔符 === {"supplier":"华东供应商","order_status":["pending","paid","shipped"],"amount_range":{"min":100,"max":9999},"is_vip":true,"remark":null,"deadline":"2025-06-30"}5.3 运行结果给我带来的三个复现结论
看完结果,有几个细节值得写进笔记:
第一,None变成了null。这是 JSON 的标准格式。Python 里的None在 JavaScript / Java 体系里对应null,json 模块会做自动转换。
第二,True变成了true。注意大小写。JSON 标准里布尔值是小写true/false,Python 里是大写True/False。序列化会自动转换,反序列化时也会自动转回。
第三,嵌套字典也能被正确处理。amount_range这个嵌套结构在两种separators设置下都能正常输出,说明 json 模块是递归遍历整个数据结构的。这保证了多层级的数据也能无损序列化。
另外,我们注意到输入里的deadline的值是字符串"2025-06-30",不是 Python 的datetime对象。如果你直接传date.today()这种对象给json.dumps,会报TypeError: Object of type date is not JSON serializable。这是个非常常见的坑,后面第 7 部分会专门讲。
6. 实际业务落地:filter_dict 在工程里的三种典型用法
6.1 用法一:把筛选条件序列化后写入日志
在真实的后端系统里,用户每次发起列表查询,都会带上一堆筛选条件。为了审计和排查问题,我们需要把筛选条件打进日志。使用ensure_ascii=False后,日志内容对运维和开发都非常友好:
import json import logging logger = logging.getLogger(__name__) def search_orders(filter_dict): # 记录请求参数,方便后期排查 logger.info("Search orders with filter: %s", json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':'))) # 省略后续查询逻辑...在 ELK 这类日志系统里,如果日志里是\u534e\u4e1c这种内容,搜索"华东"两个字根本搜不到。保持中文原样输出,直接就能在 Kibana 里做全文检索,排查问题的体验完全不一样。
6.2 用法二:构造 API 请求体
当你的 Python 服务需要调用下游 HTTP 接口时,请求体几乎都是 JSON 字符串。紧凑格式能减少网络传输字节数:
import json import requests def call_analytic_service(filter_dict): url = "https://api.example.com/v1/analytics/query" payload = json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':')) headers = {"Content-Type": "application/json"} resp = requests.post(url, data=payload, headers=headers) print(resp.json())这里有个小建议:requests库的json=参数会自动帮你做序列化,但它默认用的是ensure_ascii=True。如果你传的是纯英文字段还好,如果有中文,最好手动用json.dumps先序列化再放进data=,这样能完全掌控序列化行为。
6.3 用法三:将数据写入文件做离线分析
数据分析场景里,经常要把用户筛选条件保存下来供后续训练模型或做统计报表。紧凑格式在这里既省空间又保留可读性:
import json from pathlib import Path def save_filter_snapshot(filter_dict, file_path): data = json.dumps(filter_dict, ensure_ascii=False, separators=(',', ':'), sort_keys=True) Path(file_path).write_text(data, encoding="utf-8")sort_keys=True是我额外加的参数。它能保证字典里的 key 按字母序排列,这样同一份数据不管生成顺序如何,最终落地文件的内容都一样。这在做数据比对、增量同步时非常有用——重复生成的 JSON 文件可以直接用 diff 工具对比差异。
6.4 使用 JSONL 格式时的独特优势
顺便说一个工程里容易被忽略的好用技巧:当你要把大量 JSON 逐行写入文件(JSONL 格式)时,separators=(',', ':')几乎是标配。每行一个 JSON 对象,用紧凑格式能保证行内不换行,方便逐行读取和处理:
import json with open("events.jsonl", "w", encoding="utf-8") as f: for event in events: line = json.dumps(event, ensure_ascii=False, separators=(',', ':')) f.write(line + "\n")如果保留默认的separators,JSON 里会有空格,虽然不影响行解析,但文件体积更大。JSONL 配合紧凑格式,是处理海量事件日志的推荐组合。
7. 常见问题与排查技巧实录
7.1 传入非基础类型数据导致 TypeError
报错信息:
TypeError: Object of type date is not JSON serializable原因:json.dumps默认只能处理dict、list、str、int、float、bool、NoneType。碰到datetime、Decimal、set等类型会直接报错。
解决方案:给default参数传一个转换函数:
import json from datetime import datetime, date def json_default(obj): if isinstance(obj, (datetime, date)): return obj.isoformat() if isinstance(obj, set): return list(obj) if isinstance(obj, Decimal): return float(obj) raise TypeError(f"Type {type(obj)} not serializable") data = {"created_at": datetime.now(), "tags": {"a", "b"}} print(json.dumps(data, default=json_default, ensure_ascii=False))default参数的作用是:当遇到无法序列化的对象时,调用它来做自定义转换。把datetime转成 ISO 格式字符串是最常见的做法。
7.2 中文写入文件后变乱码
现象:ensure_ascii=False已经设置了,控制台打印正常,但用记事本打开 JSON 文件还是乱码。
原因:文件写入时指定了系统默认编码(Windows 下是gbk)或者终端工具用错了解码方式。
解决方案:写入文件时必须显式指定encoding="utf-8":
import json # 错误示范 with open("data.json", "w") as f: f.write(json.dumps({"region": "华东"}, ensure_ascii=False)) # 正确示范 with open("data.json", "w", encoding="utf-8") as f: f.write(json.dumps({"region": "华东"}, ensure_ascii=False))另外,读取时同样需要指定encoding="utf-8",保持编解码一致。
7.3 排序不稳定:明明同一份数据,输出却不同
现象:两次执行json.dumps同一个字典,输出结果的 key 顺序不同。
原因:Python 3.7 之前字典不保证顺序;Python 3.7+ 字典默认保持插入顺序,但插入顺序本身可能不同。如果你的程序里字典是通过不同路径构建的,顺序自然不同。
解决方案:使用sort_keys=True强制按键排序。这不仅让输出更稳定,还能让 JSON 在文本对比工具中更容易 diff。
print(json.dumps(filter_dict, sort_keys=True, ensure_ascii=False, separators=(',', ':')))7.4 浮点数精度丢失
现象:序列化和反序列化后,浮点数精度发生变化。比如0.1 + 0.2的结果0.30000000000000004。
原因:这是 IEEE 754 浮点数表示法的固有问题,不是 json 模块的 bug。
解决方案:对精度要求高的业务,用Decimal并在default函数中转为字符串:
from decimal import Decimal def json_default(obj): if isinstance(obj, Decimal): return str(obj) # 转为字符串,保留原始精度 raise TypeError(...) data = {"price": Decimal("199.90")} print(json.dumps(data, default=json_default))7.5 常见问题速查表
| 问题类型 | 典型现象 | 核心处理方案 |
|---|---|---|
| 中文被转义 | 输出\uXXXX,不可读 | 设置ensure_ascii=False |
| 文件乱码 | 打开文件后中文异常 | 写入时指定encoding="utf-8" |
| 非序列化类型 | TypeError: Object of type X | 使用default参数自定义转换 |
| key 顺序不稳定 | 多次输出顺序不同 | 设置sort_keys=True |
| 输出体积过大 | JSON 字符串很长 | 使用separators=(',', ':')紧凑模式 |
| 浮点精度丢失 | 小数位异常 | 用Decimal结合default转字符串 |
7.6 关于 ensure_ascii 的一个反向思考
有些开发者会担心:ensure_ascii=False输出的中文在网络上传输会乱码。其实这个担心是多余的——UTF-8 编码的中文在 HTTP 协议里完全正常。只要发送端和接收端都使用 UTF-8,就不会有问题。真正导致乱码的往往是编码声明不一致,比如一端用 GBK,另一端用 UTF-8,这时候不管你ensure_ascii设成什么,都会出问题。
所以我的建议是:在团队内部统一使用ensure_ascii=False,并且统一字符编码为 UTF-8。这样调试和日志的可读性都能得到保障,且不会带来传输副作用。
8. 写在最后的一点实操心得
做 Python 开发这些年,我最大的体会是:真正影响开发效率的往往不是那些高大上的框架,而是这些最基础、最常用的接口到底用得好不好。json.dumps这行代码看起来简单,但如果能把ensure_ascii、separators、sort_keys、default这几个参数理解透彻并灵活组合,在日常项目里能省下非常多的时间。
我个人平时还会把下面这个小工具函数放在项目公共模块里,用它来统一项目里的 JSON 序列化行为:
import json def dump_json(data, *, ensure_ascii=True, compact=False, sort_keys=False): if compact: separators = (',', ':') else: separators = None return json.dumps(data, ensure_ascii=ensure_ascii, separators=separators, sort_keys=sort_keys)这样团队里每个人调用时,只要显式声明自己想要的格式,其他细节由函数处理,不容易出偏差。
最后再分享一个小技巧:在调试输出字典内容时,可以用pprint模块搭配json.dumps,先序列化再格式化打印,效果比直接print一个大字典清晰得多。这个方法帮我在排查复杂嵌套数据时节省了不少眼睛疲劳度。