← 返回题目列表

Python 里 str() 和 repr() 有什么区别?__str__ 和 __repr__ 怎么选?

高频 中等 第 10 / 21 题 更新于 2026/07/31
Pythonstrrepr魔术方法

简化版

**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__;② 在交互式解释器直接敲 xrepr(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(printf"{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__」,就掌握了回退机制。

四、怎么写好 reprstr

理解实现的最佳实践:

__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 看清真实内容」。