Python 格式化的转换顺序:加了 !r,为什么数字格式 d 反而不能用了
给调试输出加上!r以后,原本能使用的整数格式04d突然报错;自定义对象的对齐逻辑也不再被调用。原因是!r不是给最终文字加上一层引号,它先把原对象转换为repr字符串,再让字符串处理后面的格式规范。理解转换和格式化的先后关系,可以避免日志辅助代码悄悄改变对象的格式协议,也能更准确地定位所谓“格式不支持”的异常。
本文在Linux与CPython 3.12.14上实跑,只使用固定本地样本,不读取业务数据或调用网络服务。输出来自实际执行,文中明确区分函数当前实现行为与应用自行设定的输入规则。 示例使用f-string语法,相关转换字段的顺序与标准字符串格式说明一致。Probe只记录方法调用,不执行任何外部副作用。
AI生成的概念示意图:抽象对象先变成文本纸带,再进入仅容纳文字的对齐框,表示转换改变了后续格式化对象;不是真实软件界面或运行截图。
完整程序与实际输出
保存为demo.py,执行python3 demo.py。程序只使用固定测试输入,代码和本次输出分别列出。
events = []
class Probe:
def __format__(self, spec):
events.append('format:' + spec)
return 'custom[' + spec + ']'
def __repr__(self):
events.append('repr')
return 'PROBE'
def __str__(self):
events.append('str')
return 'plain'
obj = Probe()
print('direct:', f'{obj:>8}')
print('events:', events)
events.clear()
print('repr conversion:', repr(f'{obj!r:>8}'))
print('events:', events)
events.clear()
print('str conversion:', repr(f'{obj!s:>8}'))
print('events:', events)
print('integer:', f'{42:04d}')
try:
print(f'{42!r:04d}')
except ValueError as exc:
print('repr plus numeric format:', type(exc).__name__)本次实际标准输出:
direct: custom[>8] events: ['format:>8'] repr conversion: ' PROBE' events: ['repr'] str conversion: ' plain' events: ['str'] integer: 0042 repr plus numeric format: ValueError
原样格式化把规范交给对象
Probe实现__format__,把收到的spec写入events,然后返回custom加上规范文字。直接使用带右对齐宽度八的格式时,输出是custom[>8],调用记录只有format:>8。这说明Python把规范交给对象自己的格式化方法,而不是保证任何自定义对象都自动按通用字符串规则对齐。
示例里的__format__故意不实施真正的宽度填充,目的是让调用路径容易观察。对象可以自行定义支持哪些规范、如何解释以及何时拒绝。格式微语言虽然为内置类型提供常见规则,但自定义类型仍负责实现自身语义。看到格式里写了宽度,不代表任何对象都会自动遵守同样排版。
这一点在带单位数值、日期包装类或敏感对象上很重要。类可能通过__format__控制单位、舍入或脱敏策略。调用者若加了转换标志,后续处理的就不再是原对象,需要检查是否仍符合原类想表达的输出契约,而不能只根据结果看起来是字符串就认为等价。
!r与!s先换对象,再应用格式
带!r的例子只记录repr,没有记录format。原对象先通过__repr__变成PROBE字符串,再由字符串格式化应用右对齐宽度八,得到三个空格加PROBE。为了让前导空格可见,最外层再对最终字符串调用repr,这一步不是对Probe再次调用repr,因此事件列表仍只有一次。
带!s时路径类似,只是先调用__str__得到plain,再对这个字符串做右对齐。输出中的三个空格来自字符串宽度规则,而不是Probe的__format__。!r与!s决定转换方式,冒号后面的部分决定转换结果怎样展示,两者属于不同阶段。
因此,!r不是“在原格式化结果外面显示引号”,!s也不是“保证先走自定义格式再转成字符串”。需要保留自定义格式语义时,应避免无意加入转换标记;若明确想绕开对象格式化、观察它的调试表示,!r正好能够表达这个选择。关键是让用途清楚,而不是把所有日志统一机械加上!r。
整数格式为什么在转换后失效
整数四十二直接使用04d得到0042,说明整数类型认识十进制整数格式。加上!r以后,先得到的是字符串42,接下来d格式被交给字符串处理。字符串没有整数d表示方式,因此抛出ValueError。异常并不意味着原数值不能转整数,也不意味着宽度四有问题,而是接收规范的类型已经变了。
修复方式取决于真实目标。若要带零填充的数字,直接让整数处理04d;若要展示repr文本,再选择字符串能够接受的对齐或宽度规范。不要在异常之后盲目调用int把它转回来,那会掩盖格式管道设计问题,也可能破坏自定义对象、非十进制表示或本来就该保留的文字。
格式化结果不应反过来承担数据校验。能用d输出,不表示数值处在业务允许范围;!r成功也不表示对象适合被写入某种结构化协议。若最终需要JSON、CSV或数据库参数,应使用对应序列化或绑定接口,而不是把调试文字当作数据格式。调试表示可以变化,也未必可逆。
让日志和自定义类型拥有明确边界
实现__repr__、__str__与__format__时,应分别考虑开发者观察、普通文字与带规范展示的目的。它们可以共享内部逻辑,但不应让读者误以为总是互相串行调用。本文的events观察器就是一份小型协议测试,能在类重构后迅速验证哪些入口仍被调用。
这些方法都是可执行代码,可能计算昂贵内容、读取状态或抛出异常。即便日志级别最终不输出,一条提前构造的f-string也已经触发了格式化。对性能敏感对象,应评估日志调用位置与惰性格式化方案;对敏感数据,也要检查repr是否会暴露字段,不能把“只是调试”当作泄露许可。
字符串宽度是格式协议中的字符宽度概念,并不等同于终端像素、汉字显示列宽或表情的视觉宽度。本文使用纯ASCII单词,是为了隔离转换顺序。将样本换成中文或复杂Unicode组合时,应该单独验收界面排版,不要把类型转换问题与显示测量问题混为一谈。
测试可覆盖空规范、类型支持的规范、明确不支持的规范、!r、!s以及对象方法主动抛错。既断言最终文字,也检查调用记录,才能发现输出碰巧相同但路径已经改变的情况。尤其当对象的__format__包含单位或脱敏逻辑时,调用路径本身就是重要的正确性条件。
在代码审查里,看到同一字段从原样格式化改成!r,应把它视为一次语义变化,而不只是排版调整。确认是谁接收格式规范、输出面向谁以及是否保留类型专用规则,再决定修改。这个小小转换符的价值在于明确选择表示层,风险也正来自它能够悄悄替换表示层。
参考资料与验证记录
官方资料核验于2026年10月3日。本次完整程序退出码为0,标准错误为空;例子验证的是上述固定输入与运行环境,不表示所有平台和版本的输出细节完全一致。


