Python 里 str() 和 repr() 有什么区别?__str__ 和 __repr__ 怎么选?
简化版
**str() 面向「用户」——返回友好、易读的字符串(用于展示);repr() 面向「开发者」——返回明确、无歧义的字符串(用于调试),理想情况下 repr() 的结果能被 eval() 重新求值还原出对象。**它们分别调用对象的 __str__ 和 __repr__ 方法。经典例子:s = "hello\n"——str(s) 是 hello 加一个真正的换行(打印出来是两行),而 repr(s) 是 'hello\n'(带引号、\n 显示为字面的转义序列,一眼看清字符串的真实内容)。关键规则:① print(x) 和 str(x)、f-string {x} 用 __str__;② 在交互式解释器直接敲 x、repr(x)、容器里的元素([s] 显示 ['hello\n'])用 __repr__;③ __str__ 没定义时会回退到 __repr__(所以只实现一个的话优先实现 __repr__);④ f-string 里 {x!r} 强制用 repr、{x!s} 强制用 str。实现建议:__repr__ 写成 ClassName(field=value, ...) 的形式(明确、可还原);__str__ 写用户友好的展示(可选)。核心记忆:str 给用户看(友好)、repr 给开发者看(明确可还原);print 用 str、调试/容器用 repr;只实现一个就实现 repr(str 会回退到它)。
详细版
str vs repr:
| 方面 | str() / __str__ | repr() / __repr__ |
|---|---|---|
| 面向 | 用户(可读、友好) | 开发者(明确、无歧义) |
| 目标 | 展示 | 调试;理想可 eval 还原 |
| 调用场景 | print、str()、f{x} | 交互式回显、repr()、容器元素、f{x!r} |
| 回退 | — | str 未定义时回退到它 |
import datetime
# 内置类型的 str vs repr
s = "hello\nworld"
print(str(s)) # hello(换行真的换行)→ 两行 world
print(repr(s)) # 'hello\nworld'(带引号、\n 是字面转义)
d = datetime.date(2024, 1, 1)
print(str(d)) # 2024-01-01(友好)
print(repr(d)) # datetime.date(2024, 1, 1)(可 eval 还原)
# 容器里的元素用 repr
lst = ["a\n", "b"]
print(lst) # ['a\n', 'b'](元素用 repr,看清 \n)
# 自定义类
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})" # 明确、可还原
def __str__(self):
return f"({self.x}, {self.y})" # 友好展示
p = Point(1, 2)
print(p) # (1, 2) ← __str__
print(str(p)) # (1, 2) ← __str__
print(repr(p)) # Point(x=1, y=2)← __repr__
print([p]) # [Point(x=1, y=2)] ← 容器用 __repr__
print(f"{p}") # (1, 2) ← f-string 默认 str
print(f"{p!r}") # Point(x=1, y=2)← !r 强制 repr
# 只定义 __repr__,__str__ 自动回退
class Only:
def __repr__(self): return "Only()"
print(str(Only())) # Only() ← str 回退到 repr
⚠️ 核心心法:
repr是给「你自己(开发者)」看的,追求「明确、无歧义、最好能复现对象」;str是给「用户」看的,追求「好看、易读」。当你只想实现一个时,永远实现__repr__——因为__str__缺失会自动回退到__repr__,反过来不行。判断「该看到 str 还是 repr」有个简单规律:「输出给人看的正文」用 str(f"{x}"、日志正文),「调试/开发者上下文」用 repr(交互式敲变量名、repr()、以及关键的——容器里的元素永远用 repr)。容器用 repr 是为了「无歧义」:print(["1", 1])显示['1', 1],一眼区分字符串'1'和整数1,如果元素用 str 就会变成[1, 1]分不清。一个好的__repr__惯例是写成类名(参数=值)的形式,让人看到就知道怎么重建这个对象(eval(repr(obj)) == obj是理想目标,不强求)。
完整版教学
一、str 与 repr 的定位
先理解两者的根本定位:
str():面向"用户"
目标:友好、易读的展示
"这个对象展示给最终用户看,应该长什么样"
例:日期 → "2024-01-01"、金额 → "¥99.9"
repr():面向"开发者"
目标:明确、无歧义,理想可复现对象
"这个对象在调试/日志里,应该怎么明确表示"
例:日期 → "datetime.date(2024, 1, 1)"(能看出类型和构造)
理想目标(repr):
eval(repr(obj)) == obj
→ repr 的结果能被 eval 还原成等价对象
(内置类型多数满足:repr([1,2]) → '[1, 2]',eval 回来是 [1,2])
(不强求,复杂对象给出明确表示即可)
一句话区分:
str 好看(给人看)、repr 明确(给开发者/机器看)
对同一个对象:
str 可能"丢信息换可读"(如字符串 str 不带引号)
repr 保留"精确信息"(带引号、显示转义、显示类型)
内置类型的差异例子:
str: str("a") → a repr: repr("a") → 'a'(带引号)
str: str(1) → '1' repr: repr(1) → '1'(数字一样)
→ 字符串差异最明显(引号、转义)
所以 str 给用户(友好)、repr 给开发者(明确、理想可 eval 还原)
str 与 repr 的根本定位:str() 面向「用户」——目标友好、易读的展示(日期 → “2024-01-01”);repr() 面向「开发者」——目标明确、无歧义、理想可复现对象(日期 → “datetime.date(2024, 1, 1)“、能看出类型和构造)。理想目标(repr):eval(repr(obj)) == obj(repr 结果能被 eval 还原成等价对象,内置类型多数满足,不强求)。一句话区分:str 好看(给人看)、repr 明确(给开发者/机器看)。对同一对象:str 可能「丢信息换可读」(字符串 str 不带引号)、repr 保留精确信息(带引号、显示转义、显示类型)。理解「str 给用户(友好易读)、repr 给开发者(明确无歧义、理想 eval(repr(obj))==obj);str 好看、repr 明确;字符串差异最明显(repr 带引号显示转义)」,就理解了两者的定位。
二、什么时候用哪个
理解各种场景走 str 还是 repr:
用 __str__(友好展示)的场景:
print(x) # print 调 str
str(x) # 显式
f"{x}"、"{}".format(x) # 格式化默认用 str
"%s" % x # %s 用 str
→ "输出给人看的正文"用 str
用 __repr__(明确调试)的场景:
交互式解释器直接敲 x(不加 print)→ 回显用 repr
>>> x
Point(x=1, y=2) # repr
repr(x) # 显式
容器里的元素:[x]、{x}、(x,)、{'k': x}
→ 容器的 str/repr 都对元素调 repr!
print([p]) → [Point(x=1, y=2)] # 元素用 repr
f"{x!r}" # !r 强制 repr
日志调试(推荐 repr,信息更全)
为什么容器元素用 repr:
为了"无歧义"——区分类型
print(["1", 1]) → ['1', 1] # 一眼分清 字符串'1' 和 整数 1
若用 str → [1, 1](分不清)
→ 容器展示需要明确,所以对元素用 repr
f-string / format 的转换标志:
f"{x}" → str(默认)
f"{x!r}" → repr(强制)
f"{x!s}" → str(强制)
f"{x!a}" → ascii(repr 但非 ASCII 转义)
所以 print/f{x}用 str、交互回显/容器元素/repr()用 repr、!r 强制 repr
用 __str__(友好展示)的场景:print(x)、str(x)、f"{x}"/format、"%s" % x——「输出给人看的正文」用 str。用 __repr__(明确调试)的场景:交互式解释器直接敲 x(回显用 repr)、repr(x)、容器里的元素([x]、{x}——容器的 str/repr 都对元素调 repr)、f"{x!r}"、日志调试。为什么容器元素用 repr:为了「无歧义」区分类型——print(["1", 1]) → ['1', 1](一眼分清字符串 ‘1’ 和整数 1)、若用 str 变 [1, 1] 分不清。f-string/format 的转换标志:{x} → str(默认)、{x!r} → repr(强制)、{x!s} → str(强制)、{x!a} → ascii。理解「print/str()/f{x}/%s 用 str;交互回显/repr()/容器元素/f{x!r}用 repr;容器元素用 repr 为无歧义区分类型;!r 强制 repr、!s 强制 str」,就掌握了各场景的选择。
三、str 回退到 repr
理解回退机制——只实现一个就实现 repr:
回退机制:__str__ 未定义时,str() 回退调用 __repr__
class Only:
def __repr__(self):
return "Only(data=1)"
# 没有 __str__
o = Only()
str(o) → "Only(data=1)" # 回退到 __repr__
print(o) → Only(data=1) # print 用 str → 回退 repr
repr(o) → "Only(data=1)"
反过来不成立:
只定义 __str__、不定义 __repr__:
repr(o) → 默认的 <__main__.X object at 0x...>(没帮助)
→ __repr__ 不会回退到 __str__
结论:如果只实现一个,实现 __repr__
① __repr__ 是"兜底"(str 会回退到它)
② 保证调试/容器/交互都有意义的输出
③ __str__ 是"锦上添花"(需要特别友好的展示时才加)
object 的默认实现:
默认 __repr__:<模块.类名 object at 内存地址>
→ 没信息量(只有类型和地址)
默认 __str__:调用 __repr__(即上面那个)
→ 所以不实现任何一个,print 出来是 <X object at 0x...>
实践优先级:
1. 先实现 __repr__(明确、最好可复现)
2. 需要用户友好展示再加 __str__
所以__str__未定义回退到__repr__(反之不行),只实现一个就实现__repr__
回退机制:__str__ 未定义时,str() 回退调用 __repr__——只定义 __repr__ 的类,str(o)、print(o) 都回退到 __repr__。反过来不成立:只定义 __str__ 不定义 __repr__ 时,repr(o) 是默认的 <X object at 0x...>(没帮助、__repr__ 不会回退到 __str__)。结论:如果只实现一个,实现 __repr__——① __repr__ 是兜底(str 会回退到它)、② 保证调试/容器/交互都有意义的输出、③ __str__ 是锦上添花。object 的默认实现:默认 __repr__ 是 <模块.类名 object at 内存地址>(没信息量)、默认 __str__ 调 __repr__。实践优先级:先实现 __repr__(明确、最好可复现)、需要用户友好展示再加 __str__。理解「str__未定义回退到__repr(反之不行);只实现一个就实现__repr__(是兜底、保证调试/容器/交互有意义);默认__repr__是<X object at 地址>没信息量;先__repr__再按需__str__」,就掌握了回退机制。
四、怎么写好 repr 和 str
理解实现的最佳实践:
__repr__ 的写法(明确、可复现):
惯例:写成 "类名(参数=值, ...)" 的形式
def __repr__(self):
return f"Point(x={self.x!r}, y={self.y!r})"
→ 看到就知道怎么重建对象
→ 用 !r 让字段也用 repr(字符串带引号,更精确)
理想(能 eval 还原):
eval("Point(x=1, y=2)") 能重建等价对象
→ 不强求,但尽量明确
__str__ 的写法(友好):
只在"需要特别友好的展示"时才实现
def __str__(self):
return f"({self.x}, {self.y})" # 简洁友好
→ 用户看的、报表、UI 展示
字段用 !r 的原因:
def __repr__(self):
return f"User(name={self.name!r})"
User(name='Tom') # name 带引号(!r)→ 明确是字符串
vs f"User(name={self.name})" → User(name=Tom)(看不出是字符串)
dataclass 自动生成 __repr__:
@dataclass
class Point:
x: int; y: int
→ 自动生成 __repr__ = "Point(x=1, y=2)"(省手写)
调试友好的额外技巧:
reprlib:限制超长 repr(大集合截断显示)
__repr__ 里避免昂贵计算/副作用(调试时频繁调用)
所以__repr__写"类名(字段=值)"用!r、__str__写友好展示、dataclass 自动生成 repr
__repr__ 的写法(明确、可复现):惯例写成 "类名(参数=值, ...)" 的形式——f"Point(x={self.x!r}, y={self.y!r})"(看到就知道怎么重建、用 !r 让字段也用 repr、字符串带引号更精确);理想能 eval 还原(不强求但尽量明确)。__str__ 的写法(友好):只在需要特别友好展示时实现(f"({self.x}, {self.y})"、用户看的/报表/UI)。字段用 !r 的原因:f"User(name={self.name!r})" → User(name='Tom')(name 带引号明确是字符串),vs 不带 → User(name=Tom)(看不出是字符串)。dataclass 自动生成 __repr__:@dataclass 自动生成 "Point(x=1, y=2)"(省手写)。调试友好技巧:reprlib 限制超长 repr、__repr__ 里避免昂贵计算/副作用。理解「__repr__写’类名(字段=值)‘用!r 让字段带引号、str__写友好展示;dataclass 自动生成__repr;__repr__避免昂贵计算/副作用」,就掌握了实现最佳实践。
五、经典例子——字符串的 str vs repr
用字符串这个最典型的例子深化理解:
字符串的 str vs repr(差异最明显):
s = "hello\nworld\t!"
str(s):返回字符串本身(无引号、转义生效)
print(str(s)):
hello
world ! # \n 真换行、\t 真制表
→ 展示给用户看的"实际文本"
repr(s):带引号、转义显示为字面
print(repr(s)):
'hello\nworld\t!' # 带引号、\n \t 是字面字符
→ 明确看到字符串的"真实构成"(有哪些特殊字符)
为什么 repr 对字符串这么有用(调试):
① 一眼看出是"字符串"(有引号)
② 看清有没有隐藏的空格/换行/制表符
" spaces " 的 repr → ' spaces '(前后空格可见)
③ 区分 "1"(字符串)和 1(数字)
→ 调试时用 repr 打印字符串,避免"看起来一样其实不同"
实战:调试时该用 repr
print(f"got: {value}") # str,看不清特殊字符
print(f"got: {value!r}") # repr,看清真实内容(推荐调试用)
logging.debug("x=%r", x) # %r 用 repr
容器打印时元素自动用 repr:
print(["hello\n"]) → ['hello\n'] # 元素 repr,看清 \n
print("hello\n") → hello + 换行 # 顶层 str
所以字符串:str 无引号转义生效(展示)、repr 带引号显示转义(调试看清真实内容)
字符串的 str vs repr(差异最明显):s = "hello\nworld\t!"——str(s) 返回字符串本身(无引号、转义生效)(print(str(s)) 会真换行、真制表,展示给用户的实际文本);repr(s) 带引号、转义显示为字面('hello\nworld\t!',明确看到字符串的真实构成)。为什么 repr 对字符串这么有用(调试):① 一眼看出是字符串(有引号)、② 看清有没有隐藏的空格/换行/制表符(" spaces " 的 repr 前后空格可见)、③ 区分 “1”(字符串)和 1(数字)。实战:调试时该用 repr——f"got: {value!r}" 看清真实内容、logging.debug("x=%r", x) 用 repr。容器打印时元素自动用 repr。理解「字符串 str 无引号转义生效(展示)、repr 带引号显示转义(调试看清真实内容:是字符串+隐藏空格换行+区分’1’和 1);调试用{value!r}或%r」,就掌握了这个经典例子。
六、总结与实践
总结 str 和 repr:
核心区别:
str()/__str__:给用户,友好可读(展示)
repr()/__repr__:给开发者,明确无歧义(调试,理想可 eval 还原)
调用场景:
用 str:print、str()、f"{x}"、"%s"(输出正文)
用 repr:交互回显、repr()、容器元素、f"{x!r}"、日志调试
回退:
__str__ 未定义 → 回退 __repr__(反之不行)
→ 只实现一个就实现 __repr__
实现建议:
__repr__:写 "类名(字段=值)"(用 !r 让字段精确),
尽量可复现(eval 理想)
__str__:需要用户友好展示时才加
dataclass 自动生成 __repr__
调试技巧:
打印字符串/变量用 !r 或 %r(看清真实内容)
容器打印时元素自动用 repr(无歧义)
核心总结:
str 好看给用户、repr 明确给开发者
print 用 str、调试/容器用 repr
只实现一个实现 __repr__(str 回退到它)
__repr__ 写"类名(字段=值)"、调试用 !r
所以 str 友好给用户、repr 明确给开发者;只实现一个实现__repr__;调试用!r
核心区别:str()/__str__ 给用户(友好可读、展示)、repr()/__repr__ 给开发者(明确无歧义、调试、理想可 eval 还原)。调用场景:用 str(print、str()、f”{x}”、%s)、用 repr(交互回显、repr()、容器元素、f”{x!r}“、日志调试)。回退:__str__ 未定义回退 __repr__(反之不行)、只实现一个就实现 __repr__。实现建议:__repr__ 写「类名(字段=值)」(用 !r 让字段精确、尽量可复现)、__str__ 需要友好展示才加、dataclass 自动生成 repr。调试技巧:打印字符串/变量用 !r 或 %r(看清真实内容)、容器元素自动用 repr。理解「str 好看给用户、repr 明确给开发者;print 用 str、调试/容器用 repr;只实现一个实现__repr__(str 回退);__repr__写类名(字段=值)、调试用!r」,就掌握了总结与实践。
记忆钩子:「str()面向『用户』(友好易读、用于展示)、repr()面向『开发者』(明确无歧义、用于调试,理想 eval(repr(obj))==obj 能还原对象),分别调用__str__和__repr__;★经典例子字符串 s=‘hello\n’:str(s)是 hello 加真换行(展示实际文本)、repr(s)是’hello\n’(带引号+\n 显示为字面转义,一眼看清真实构成);★哪个场景用哪个:print(x)/str(x)/f’{x}’/%s 用__str__(输出给人看的正文);交互式直接敲 x 回显、repr(x)、★容器里的元素([s]显示[‘hello\n’])、f’{x!r}‘用__repr__(容器用 repr 是为无歧义区分类型,print([‘1’,1])显示[‘1’,1]分清字符串和整数);★关键规则:str__没定义时 str()会回退到__repr(反之不行!),所以只实现一个就实现__repr__(它是兜底、保证调试/容器/交互有意义);实现建议:__repr__写成’类名(字段=值)‘形式(用!r 让字段带引号更精确)、str__写用户友好展示、dataclass 自动生成__repr;调试打印字符串用{value!r}或 logging 的%r 看清隐藏的空格/换行、区分’1’和 1」。
七、常见误区与追问
- 误区:str() 和 repr() 对所有对象返回一样的结果。 对很多对象不同——最典型是字符串:
str("a\n")是 a 加真换行、repr("a\n")是'a\n'(带引号、转义显示为字面);日期str是 “2024-01-01”、repr是 “datetime.date(2024, 1, 1)“;数字等简单类型两者可能相同(str(1)和repr(1)都是 ‘1’);总体上 str 追求「好看给用户」、repr 追求「明确给开发者、理想可还原」。 - 误区:print 一个对象会调用它的 repr。 调用
__str__——print(x)、str(x)、f"{x}"、"%s" % x都走__str__;__repr__用在交互式解释器直接回显变量、repr(x)、容器里的元素、f"{x!r}"、日志调试等;不过如果对象没定义__str__,print会因回退机制而用__repr__。 - 误区:只实现 str 就够了。 更应该先实现
__repr__——因为__str__未定义时str()会自动回退到__repr__(反过来__repr__不会回退到__str__);如果只实现__str__,那么repr(obj)、交互式回显、容器里显示这个对象时都会用默认的<X object at 0x...>(毫无信息量);所以「只实现一个」时永远实现__repr__,它能覆盖调试、容器、交互等更多场景;需要特别友好的用户展示时再加__str__。 - 误区:容器(列表/字典)打印时,里面的元素用 str 显示。 用 repr——
print(["1", 1])显示['1', 1](字符串元素带引号、和整数区分开),而不是[1, 1];这是故意的:容器展示追求「无歧义」,用 repr 让人一眼看清每个元素的类型和真实内容(有没有隐藏的空格、换行);所以自定义类只实现了友好的__str__而没实现__repr__时,放进列表打印会显示难看的默认 repr。 - 追问:
eval(repr(obj)) == obj是什么意思,一定要满足吗? 这是__repr__的「理想目标」:repr 返回的字符串如果能被eval()重新求值、得到一个和原对象相等的对象,说明这个 repr 是「无歧义、可复现」的;很多内置类型满足(eval(repr([1,2]))得到[1,2]、eval(repr("a"))得到"a");自定义类把__repr__写成"ClassName(x=1, y=2)"形式就接近这个目标(前提是该构造调用在当前作用域可用);但不强求——很多对象(文件句柄、连接、复杂状态)无法或不必可复现,这时 repr 只要给出「明确的、包含关键信息的表示」即可(如<Connection host='localhost' port=5432>),重点是调试时一眼看清对象是什么。 - 追问:写 repr 时字段为什么建议用 !r(如 f”User(name={self.name!r})”)? 因为用
!r会对字段本身也调用 repr、让字符串字段带上引号,从而「明确表示类型、避免歧义」:f"User(name={self.name!r})"输出User(name='Tom')(一眼看出 name 是字符串 ‘Tom’),而f"User(name={self.name})"输出User(name=Tom)(看不出 Tom 是字符串还是别的、也看不出有没有前后空格);repr 的宗旨就是无歧义,字段用!r能让嵌套的字符串、特殊字符都精确显示,也更接近「可 eval 还原」的目标(User(name='Tom')能被 eval,User(name=Tom)里 Tom 会被当成变量名)。 - 追问:调试时打印变量,为什么推荐用
f"{x!r}"或logging的%r而不是普通{x}? 因为调试要「看清真实内容」而普通 str 会隐藏关键信息:用 repr 打印字符串能显示引号(区分字符串和数字)、显示转义字符(看到\n、\t而不是真的换行/制表)、暴露前后隐藏的空格(' x ');比如变量看起来是hello但可能实际是'hello '(带尾空格)或'hello\n',用{x!r}一眼就能发现,用{x}则看起来「正常」却查不出 bug;所以调试日志、断言信息里打印字符串/未知类型的变量一律用!r(f-string)或%r(logging/百分号格式化),能省下大量「看起来一样其实不同」的排查时间。
八、加强记忆
str() 面向「用户」(友好、易读、用于展示),repr() 面向「开发者」(明确、无歧义、用于调试,理想 eval(repr(obj)) == obj 能还原对象),分别调用 __str__ 和 __repr__。经典例子字符串 s = "hello\n":str(s) 是 hello 加真换行(展示实际文本)、repr(s) 是 'hello\n'(带引号 + \n 显示为字面转义、一眼看清真实构成)。哪个场景用哪个:print(x)/str(x)/f"{x}"/%s 用 __str__(输出给人看的正文);交互式直接敲 x 回显、repr(x)、容器里的元素([s] 显示 ['hello\n'])、f"{x!r}" 用 __repr__(容器用 repr 是为无歧义区分类型,print(["1", 1]) 显示 ['1', 1] 分清字符串和整数)。关键规则:__str__ 没定义时 str() 会回退到 __repr__(反之不行),所以只实现一个就实现 __repr__(它是兜底、保证调试/容器/交互有意义)。实现建议:__repr__ 写成 "类名(字段=值)" 形式(用 !r 让字段带引号更精确)、__str__ 写用户友好展示、dataclass 自动生成 __repr__;调试打印字符串用 {value!r} 或 %r(看清隐藏的空格/换行、区分 ‘1’ 和 1)。一句话「str 给用户(友好)、repr 给开发者(明确可还原);print/f{x}用 str,交互回显/容器元素/repr()/f{x!r}用 repr;str__缺失回退到__repr(反之不行),只实现一个实现__repr__;__repr__写类名(字段=值)、调试用!r 看清真实内容」。