Python pprint 的可读性检查:循环列表能打印出来,为什么仍然不能靠那段文字重建
调试一个列表时,pprint顺利输出了内容,于是把那段文字保存下来,期待以后还原对象。但其中出现了Recursion提示,已经不是普通的列表字面量。格式化能够结束、文字能供人阅读、对象关系可以完整保存,是三个需要分别检查的条件。
本例在Linux、CPython 3.12.14实跑。保存为demo.py后运行python3 demo.py。只创建有限的内存容器;对本程序自己生成的简单字面量使用ast.literal_eval验证,不执行外部文本,也不对递归表示尝试求值。
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。


