Python pprint 的可读性检查:循环列表能打印出来,为什么仍然不能靠那段文字重建

昨天 3阅读

调试一个列表时,pprint顺利输出了内容,于是把那段文字保存下来,期待以后还原对象。但其中出现了Recursion提示,已经不是普通的列表字面量。格式化能够结束、文字能供人阅读、对象关系可以完整保存,是三个需要分别检查的条件。

本例在Linux、CPython 3.12.14实跑。保存为demo.py后运行python3 demo.py。只创建有限的内存容器;对本程序自己生成的简单字面量使用ast.literal_eval验证,不执行外部文本,也不对递归表示尝试求值。

Python pprint 的可读性检查:循环列表能打印出来,为什么仍然不能靠那段文字重建

AI生成的概念示意图:普通容器、自指回路和两处共享同一对象并列出现,展示三种不同关系;不是对象地址图。

完整程序与本地结果

完整可运行程序

import ast
import pprint

plain = {"items": [1, 2], "ok": True}
plain_text = pprint.pformat(plain, sort_dicts=True)
assert pprint.isreadable(plain)
assert ast.literal_eval(plain_text) == plain
print("plain readable:", pprint.isreadable(plain))
print("plain literal roundtrip: True")

loop = []
loop.append(loop)
rendered = pprint.pformat(loop)
assert "<Recursion on list with id=" in rendered
assert pprint.isrecursive(loop)
assert not pprint.isreadable(loop)
print("recursive marker present:", "<Recursion on list" in rendered)
print("loop recursive:", pprint.isrecursive(loop))
print("loop readable:", pprint.isreadable(loop))

shared = [1, 2]
twice = [shared, shared]
assert twice[0] is twice[1]
assert not pprint.isrecursive(twice)
assert pprint.isreadable(twice)
restored = ast.literal_eval(pprint.pformat(twice))
assert restored == twice and restored[0] is not restored[1]
print("shared reference recursive:", pprint.isrecursive(twice))
print("restored values equal:", restored == twice)
print("restored aliases preserved:", restored[0] is restored[1])

本次实际输出(以下为结果,不是程序)

plain readable: True
plain literal roundtrip: True
recursive marker present: True
loop recursive: True
loop readable: False
shared reference recursive: False
restored values equal: True
restored aliases preserved: False

普通字面量可以验证值的往返

plain只包含字典、列表、整数和布尔值,isreadable返回True。程序把pformat结果交给literal_eval,断言恢复的值与plain相等。这条测试条件很具体:输入来自我们刚创建的小型基础类型结构,不代表任意对象的展示字符串都有同样能力。

isreadable检查的是表示是否能够用来重建值,不是安全认证,也不是文件格式校验。不要因为返回True,就把来源不明的文本交给eval。本文选择literal_eval只是配合受控字面量实验,它同样不适合无大小限制地处理不受信任输入。

直接指向自己的列表需要递归表示

loop先是空列表,随后把自身作为成员加入。pformat可以为这种常见容器生成递归标记,而不是无止境展开。输出中的recursive marker present为True,isrecursive也为True,但isreadable为False,三个观察直接对应不同问题。

标记里含有对象身份相关数字,通常会随运行变化。程序只检查标记是否存在,不把那个数字写成固定预期,所以重复执行仍然稳定。需要比较日志时,也不要把身份数字变化误读成业务数据变化,更不能把它当作跨进程可使用的对象地址。

共享一个子列表不等于形成环

twice的两个位置引用同一个shared,身份检查为真,但shared没有指回外层,所以isrecursive返回False。这里存在共享关系,却没有沿成员继续访问后回到祖先的回路。区分这两者,对解释复杂配置与缓存对象的调试输出很有帮助。

pprint显示两份内容相同的子列表,并不承诺表达“这两处其实是同一个对象”。如果只看格式化文本,人可能以为它们完全独立;如果只看到引用复用,又把它称为递归,也会把普通共享结构误判为循环依赖。

值相等以后,别名关系仍可能丢失

把twice的格式化文本用literal_eval还原,结果在值上相等,两个内部列表的身份却不再相同。输出最后两行一真一假,证明恢复内容与恢复对象图不是一回事。之后只修改其中一个子列表,新对象与原对象就可能表现不同。

因此pformat适合帮助人检查数据,不应未经约定就充当需要保留共享引用的持久化协议。真正需要保存对象关系时,应选择适合场景的明确格式,规定引用标识、循环处理和版本规则;只把漂亮文本存盘,无法自动补齐这些约束。

递归保护有已知类型边界

官方文档对常见字典、列表、元组及特定子类描述了递归保护范围。自定义对象若重写表示方法,可能运行自己的代码,甚至在递归检测之前就出错。本文直接使用内建列表,不能据此推断任意对象交给pprint都一定安全终止。

同样,isrecursive本身也不是通用对象图分析器,官方提醒它存在与安全表示相同的限制,检测不到的递归仍可能导致RecursionError。面对业务模型,可以先提取有限的普通字段再显示,避免让调试日志隐式触发复杂属性或表示行为。

验收时保留普通结构、直接环和共享无环三种样本,分别检查输出是否产生、递归标记是否存在、值能否往返以及身份关系是否保持。这样“日志看起来正常”不会替代实际需要的数据契约,也能更准确地解释为何某一份显示结果不能直接用于恢复。

参考资料

资料核验日期:2026年10月2日。以上输出来自固定输入的本地实跑,退出码为0。

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。