06-字符串表示形式
约 3191 字大约 11 分钟
2026-09-28
一个对象可以有多种“显示方式”。开发者调试时希望看到完整、明确、尽量无歧义的信息;最终用户阅读时通常只希望看到简洁、自然的文本。
Python 通过两个主要特殊方法处理这两种需求:
__repr__:面向开发者的表示,主要服务于调试、交互式控制台和容器显示;__str__:面向用户的表示,主要服务于str()、print()和普通字符串格式化。
这一节会回答:
repr(obj)和str(obj)分别做什么;- 为什么只实现
__repr__往往已经够用; print()、列表和字典为什么可能显示不同的文本;- f-string 中的默认格式、
!r和!s有什么区别; - 一个合格的
__repr__应该满足什么条件; - 日志和用户界面为什么不应简单复用同一种表示。
1. 先看一个最小对象
没有自定义字符串表示时,Python 通常只能显示对象的类型和内存地址:
class User:
def __init__(self, name):
self.name = name
user = User("Lin")
print(user)输出类似:
<__main__.User object at 0x000001...>地址每次运行都可能不同,也不能告诉我们用户的名字。为对象实现 __repr__:
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
user = User("Lin")
print(user)输出变成:
User(name='Lin')即使调用的是 print(),这里仍然会显示 __repr__ 的结果,因为类没有定义 __str__,str(user) 会回退到 __repr__。
2. repr() 和 str() 的公开入口
这两个内置函数分别请求对象的两种表示:
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
def __str__(self):
return self.name
user = User("Lin")
print(repr(user))
print(str(user))输出为:
User(name='Lin')
Lin调用链分别是:
repr(user) -> User.__repr__(user)
str(user) -> User.__str__(user)两个特殊方法都必须返回字符串对象。如果错误地返回整数或其他类型:
class Broken:
def __repr__(self):
return 42
repr(Broken())Python 会抛出 TypeError,因为对象表示协议要求返回 str,不会自动把返回值转换成字符串。
3. __repr__:给开发者看的准确表示
Python 对 __repr__ 的传统期望是:
尽可能返回一个明确的字符串,使人能够理解对象的内容;如果可行,最好能看起来像重新创建该对象的 Python 表达式。
例如:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
point = Point(2, 3)
print(repr(point))Point(2, 3)这个表示让读者可以直接看出构造所需的两个值。它不一定真的能够执行:
Point(2, 3)是否能在当前作用域中重新创建对象,取决于类是否可访问、构造函数是否完整,以及对象是否还有无法从文本恢复的运行时状态。重点是表示清楚,而不是承诺序列化能力。
!r 是设计 __repr__ 时的重要细节
在 f-string 中,!r 请求一个值的 repr():
class Config:
def __init__(self, mode):
self.mode = mode
def __repr__(self):
return f"Config(mode={self.mode!r})"
print(Config("debug"))输出为:
Config(mode='debug')如果去掉 !r:
return f"Config(mode={self.mode})"输出会变成:
Config(mode=debug)这看起来像一个没有引号的变量名,不再清楚它是字符串。对于字符串、路径、枚举值等字段,在 __repr__ 中使用 !r 通常更可靠。
4. __str__:给使用者看的友好文本
__str__ 适合输出对象的简洁描述:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __repr__(self):
return f"Temperature(celsius={self.celsius!r})"
def __str__(self):
return f"{self.celsius} °C"
temperature = Temperature(23.5)
print(repr(temperature))
print(str(temperature))输出为:
Temperature(celsius=23.5)
23.5 °C两个表示服务不同场景:
repr(temperature)适合调试和检查内部值;str(temperature)适合用户看到的自然语言输出。
__str__ 不应该为了“更好看”而隐藏调试时必需的关键状态。真正面向终端用户的文案,如果需要国际化、权限控制或复杂布局,通常应由专门的展示层负责,而不是把所有业务文本塞进 __str__。
5. 只实现 __repr__ 时会发生什么
如果一个类没有实现 __str__,Python 会从 object 继承一个默认行为:使用对象的 __repr__ 结果作为字符串表示。
class Job:
def __init__(self, name, status):
self.name = name
self.status = status
def __repr__(self):
return f"Job(name={self.name!r}, status={self.status!r})"
job = Job("build", "running")
print(repr(job))
print(str(job))
print(job)三次输出都是:
Job(name='build', status='running')
Job(name='build', status='running')
Job(name='build', status='running')这就是为什么很多值对象只实现 __repr__:它已经足够清晰,额外定义一个 __str__ 反而可能造成两种表示之间的不一致。
只有当对象确实需要一个明显不同的用户友好文本时,才添加 __str__。
6. print() 到底调用了什么
print(obj) 会将对象转换成适合普通显示的字符串,概念上相当于调用 str(obj):
class Message:
def __repr__(self):
return "Message(text='hello')"
def __str__(self):
return "hello"
message = Message()
print(message)
print(str(message))
print(repr(message))输出为:
hello
hello
Message(text='hello')因此,print() 并不是“自动选择最详细表示”,而是遵守 str() 的用户友好路径。
如果 __str__ 不存在,str(message) 才会回退到 __repr__。
print() 还支持多个参数、分隔符和结尾字符:
print("status:", message, sep=" ", end="\n")这些是 print() 本身的输出控制,不改变对象如何实现 __str__。
7. 交互式控制台通常使用 repr()
在交互式解释器中,输入一个表达式但不显式调用 print() 时,解释器会显示表达式结果的 repr():
>>> job
Job(name='build', status='running')这也是为什么调试时 __repr__ 特别重要。开发者不需要对每个对象手动写 print(repr(obj)),控制台和调试工具就能获得有用信息。
在普通 .py 脚本中,单独写一个表达式不会自动输出任何东西:
job脚本需要明确调用 print(job) 或记录日志。交互式控制台的自动显示规则不能当作脚本输出机制。
8. 容器通常使用元素的 repr()
一个容易观察到的规则是:列表、元组、集合和字典在显示内部元素时,通常使用元素的 repr(),而不是 str()。
class Message:
def __repr__(self):
return "Message(text='hello')"
def __str__(self):
return "hello"
message = Message()
print(message)
print([message])
print({"message": message})输出为:
hello
[Message(text='hello')]
{'message': Message(text='hello')}容器使用 repr() 是有意的:如果容器使用每个元素的友好文本,嵌套结构可能会丢失类型、边界和字符串引号,调试起来很困难。
因此,设计好 __repr__ 会同时改善:
- 交互式控制台显示;
- 列表和字典等容器中的对象显示;
- 测试失败时的断言信息;
- 调试器和日志中的结构化对象信息。
9. f-string 中的默认值、!s 和 !r
f-string 默认使用对象的 __format__,在没有特殊格式说明时通常表现得像 str():
class User:
def __repr__(self):
return "User(name='Lin')"
def __str__(self):
return "Lin"
user = User()
print(f"default={user}")
print(f"str={user!s}")
print(f"repr={user!r}")输出为:
default=Lin
str=Lin
repr=User(name='Lin')三个写法的含义是:
{user}:走默认格式化路径;{user!s}:先调用str(user);{user!r}:先调用repr(user)。
日志或调试输出中,如果希望确认对象类型和内部状态,可以明确写 !r:
logger_message = f"received value: {user!r}"普通用户文案则通常使用默认格式或 !s。
10. __format__ 与专用格式说明
format(obj) 和 f-string 的格式说明最终由 __format__ 处理:
format(obj, format_spec)对于只需要默认字符串表示的类型,不必立即实现它。下面的 Money 增加一个简单格式协议,让调用者选择带货币符号或只显示数值:
class Money:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f"Money({self.amount!r})"
def __str__(self):
return f"{self.amount:.2f}"
def __format__(self, spec):
if spec == "currency":
return f"¥{self.amount:.2f}"
if spec == "number":
return f"{self.amount:.2f}"
if spec == "":
return str(self)
raise ValueError(f"unknown format specifier: {spec!r}")
money = Money(12.5)
print(str(money))
print(f"{money:currency}")
print(f"{money:number}")输出为:
12.50
¥12.50
12.50这个例子说明了一个边界:
__str__定义默认的友好文本;__format__定义带格式说明时如何输出;- 自定义格式说明应当有清楚、稳定的含义;
- 对未知格式说明应明确报错,而不是静默地产生错误文本。
如果对象不需要专用格式,不要为了“完整”而实现 __format__。继承 object.__format__ 通常已经足够处理空格式说明。
11. __repr__ 中不要泄露敏感信息
调试表示很有用,但它可能出现在日志、异常消息、测试失败信息或监控平台中。因此,__repr__ 不应无条件包含密码、令牌、私钥等敏感数据。
错误示例:
class Connection:
def __init__(self, host, token):
self.host = host
self.token = token
def __repr__(self):
return f"Connection(host={self.host!r}, token={self.token!r})"更稳妥的表示:
class Connection:
def __init__(self, host, token):
self.host = host
self.token = token
def __repr__(self):
return f"Connection(host={self.host!r}, token='***')"这不是 Python 协议本身的硬性要求,而是工程上的安全要求。对象表示应当有助于诊断问题,但不应把凭证带入默认日志路径。
12. __repr__ 中避免昂贵或有副作用的操作
表示方法可能在异常处理、调试器或日志中被频繁调用。因此,__repr__ 和 __str__ 最好:
- 不执行网络请求;
- 不访问可能阻塞的外部服务;
- 不修改对象状态;
- 不触发复杂计算;
- 即使对象处于部分初始化状态,也尽量不会再次抛出异常。
例如,不要在 __repr__ 中为了显示关联对象而查询数据库。对象表示应尽量使用已经在本地可用的状态。
如果某个字段计算确实昂贵,可以显示简短的标记:
class Report:
def __init__(self, report_id):
self.report_id = report_id
def __repr__(self):
return f"Report(report_id={self.report_id!r}, details=<lazy>)"13. __repr__ 与相等性、测试输出
一个清晰的 __repr__ 不会自动让两个对象相等。表示、相等性和哈希是三个独立问题:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x!r}, {self.y!r})"
left = Point(1, 2)
right = Point(1, 2)
print(left)
print(right)
print(left == right)输出为:
Point(1, 2)
Point(1, 2)
False如果领域语义要求坐标相同的点相等,还需要单独实现 __eq__,或者使用适合的值对象工具。不要从“两个对象打印出来一样”推断它们一定可以作为字典键互换。
14. 一个同时展示多种路径的完整示例
下面的程序把控制台表示、用户表示、容器表示和格式化放在一起:
class User:
def __init__(self, username, role):
self.username = username
self.role = role
def __repr__(self):
return f"User(username={self.username!r}, role={self.role!r})"
def __str__(self):
return f"{self.username} ({self.role})"
if __name__ == "__main__":
user = User("lin", "admin")
print(repr(user))
print(str(user))
print(user)
print([user])
print(f"default: {user}")
print(f"debug: {user!r}")输出为:
User(username='lin', role='admin')
lin (admin)
lin (admin)
[User(username='lin', role='admin')]
default: lin (admin)
debug: User(username='lin', role='admin')请按调用入口理解输出:
repr(user)明确请求开发者表示;str(user)和print(user)请求友好文本;[user]是容器表示,因此使用元素的repr();- f-string 默认使用友好文本,
!r强制使用开发者表示。
15. 本节小结
对象的字符串表示不是单纯的美化工作,而是对象协议的一部分:
__repr__主要服务于开发者、调试器、交互式控制台和容器;__str__主要服务于用户友好输出;- 没有
__str__时,str(obj)会回退到__repr__; print(obj)走str()路径;- 列表、字典等容器通常使用元素的
repr(); - f-string 默认走格式化路径,
!s和!r可以明确选择表示方式; __repr__应该尽量清晰、稳定、低成本且不泄露敏感信息;- 表示方法和相等性、哈希、序列化是不同协议,不能互相替代。
下一节将继续研究真假协议:对象如何实现 bool(obj),以及没有 __bool__ 时为什么会回退到 __len__。
拓展阅读与查阅
以下资料用于进一步研究或日后查阅。理解本节内容不依赖这些链接。
