从打印对象到高质量调试:彻底理解 Python 中 `__repr__` 和 `__str__` 的区别

从打印对象到高质量调试:彻底理解 Python 中 __repr____str__ 的区别

在 Python 编程中,很多人第一次接触类时,都会遇到这样一个小尴尬:

python 复制代码
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age


user = User("Alice", 18)
print(user)

输出结果可能是:

python 复制代码
<__main__.User object at 0x104b2e550>

这是什么意思?它当然不是错,但它对我们几乎没有帮助。我们真正想看到的可能是:

python 复制代码
User(name='Alice', age=18)

或者更友好一点:

python 复制代码
用户 Alice,18 岁

这背后就涉及 Python 中两个非常重要的魔术方法:

python 复制代码
__repr__
__str__

它们都和"对象如何变成字符串"有关,但用途完全不同。简单说:

__repr__ 面向开发者,强调准确、明确、可调试。

__str__ 面向用户,强调友好、可读、易理解。

这篇文章会从基础语法讲到真实项目实践,帮助你真正理解 __repr____str__ 的区别,并掌握 Python最佳实践 中对象字符串表示的设计方法。


一、为什么对象需要字符串表示?

Python 从诞生之初就强调简洁、可读和表达力。它既可以写自动化脚本,也能支撑 Web 后端、数据科学、人工智能、运维工具和大型工程系统。

在这些场景里,调试和日志非常重要。

比如你在开发一个订单系统,日志里出现:

python 复制代码
<__main__.Order object at 0x103c97d60>

你几乎无法知道这个订单是谁的、多少钱、什么状态。

但如果日志是这样:

python 复制代码
Order(id='A001', amount=199.0, status='paid')

排查问题的效率会高很多。

这就是 __repr____str__ 的价值:它们让对象在打印、日志、调试、交互式环境中呈现出更有意义的信息。


二、先看结论:__repr____str__ 的核心区别

可以先记住一句话:

text 复制代码
__repr__:给开发者看的,目标是明确、无歧义、最好能复现对象。
__str__ :给用户看的,目标是友好、自然、适合展示。

举个例子:

python 复制代码
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product(name={self.name!r}, price={self.price!r})"

    def __str__(self):
        return f"{self.name},售价 ¥{self.price:.2f}"


p = Product("Python 实战手册", 89)

print(repr(p))
print(str(p))
print(p)

输出:

python 复制代码
Product(name='Python 实战手册', price=89)
Python 实战手册,售价 ¥89.00
Python 实战手册,售价 ¥89.00

注意最后一行:

python 复制代码
print(p)

默认调用的是:

python 复制代码
str(p)

所以它会优先使用 __str__


三、__repr__ 是什么?

__repr__ 的全称可以理解为 representation,即"表示形式"。

它的目标是:尽可能准确地表达这个对象是什么。

例如:

python 复制代码
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})"


user = User("alice", "admin")

print(repr(user))

输出:

python 复制代码
User(username='alice', role='admin')

为什么这里用了 !r

python 复制代码
f"{self.username!r}"

等价于:

python 复制代码
repr(self.username)

它会保留字符串的引号,让结果更准确。

对比一下:

python 复制代码
name = "Alice"

print(f"{name}")
print(f"{name!r}")

输出:

python 复制代码
Alice
'Alice'

__repr__ 中,使用 !r 是非常推荐的做法,因为它能减少歧义。

例如:

python 复制代码
User(username=Alice, role=admin)

就不如:

python 复制代码
User(username='Alice', role='admin')

清楚。


四、__str__ 是什么?

__str__ 的目标是:给人看的友好字符串。

比如:

python 复制代码
class User:
    def __init__(self, username, role):
        self.username = username
        self.role = role

    def __str__(self):
        return f"{self.username}({self.role})"


user = User("alice", "admin")

print(str(user))
print(user)

输出:

python 复制代码
alice(admin)
alice(admin)

__str__ 常用于:

text 复制代码
命令行输出
用户界面展示
报表文本
错误提示
业务描述

例如订单对象:

python 复制代码
class Order:
    def __init__(self, order_id, amount, status):
        self.order_id = order_id
        self.amount = amount
        self.status = status

    def __str__(self):
        return f"订单 {self.order_id}:¥{self.amount:.2f},状态:{self.status}"


order = Order("A001", 199.9, "已支付")

print(order)

输出:

python 复制代码
订单 A001:¥199.90,状态:已支付

这就很适合给业务人员、用户或运营同学看。


五、当只定义一个时,会发生什么?

这是理解二者关系的关键。

1. 只定义 __repr__

python 复制代码
class User:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"User(name={self.name!r})"


u = User("Alice")

print(repr(u))
print(str(u))
print(u)

输出:

python 复制代码
User(name='Alice')
User(name='Alice')
User(name='Alice')

如果没有定义 __str__,Python 会退而求其次使用 __repr__

所以在实际开发中,如果你只想写一个,优先写 __repr__


2. 只定义 __str__

python 复制代码
class User:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"用户:{self.name}"


u = User("Alice")

print(str(u))
print(repr(u))

输出类似:

python 复制代码
用户:Alice
<__main__.User object at 0x104b2e550>

这时 str(u) 友好了,但 repr(u) 仍然是默认内存地址形式,不利于调试。

这也是为什么我建议业务核心对象优先实现 __repr__


六、交互式环境为什么更偏爱 __repr__

在 Python REPL 或 Jupyter Notebook 中,直接输入一个对象:

python 复制代码
u

显示结果通常使用 repr(u),而不是 str(u)

原因很简单:交互式环境主要面向开发者,它更关心对象的准确表达。

python 复制代码
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x!r}, y={self.y!r})"

    def __str__(self):
        return f"({self.x}, {self.y})"


p = Point(3, 4)

print(str(p))
print(repr(p))

输出:

python 复制代码
(3, 4)
Point(x=3, y=4)

在调试中,你通常更想看到:

python 复制代码
Point(x=3, y=4)

因为它告诉你对象类型和字段含义。


七、列表、字典等容器中使用的是 __repr__

这是很多人容易忽略的细节。

看下面的代码:

python 复制代码
class User:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return f"用户:{self.name}"


users = [User("Alice"), User("Bob")]

print(users)

输出可能是:

python 复制代码
[<__main__.User object at 0x...>, <__main__.User object at 0x...>]

为什么没有使用 __str__

因为列表在显示内部元素时,调用的是元素的 repr(),不是 str()

如果想让列表打印更清晰,需要实现 __repr__

python 复制代码
class User:
    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return f"User(name={self.name!r})"

    def __str__(self):
        return f"用户:{self.name}"


users = [User("Alice"), User("Bob")]

print(users)

输出:

python 复制代码
[User(name='Alice'), User(name='Bob')]

这在 Python实战 中非常重要。日志里经常打印列表、字典、集合,如果对象没有好的 __repr__,日志质量会很差。


八、最佳实践:__repr__ 最好能帮助复现对象

理想情况下,__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})"


p = Point(1, 2)

print(repr(p))

输出:

python 复制代码
Point(1, 2)

这个结果非常清晰,甚至可以直接用于重新构造对象。

当然,真实项目里不一定每个对象都能完全复现,比如数据库连接、文件句柄、网络请求对象等。但至少应该包含关键字段。

例如:

python 复制代码
class DatabaseConnection:
    def __init__(self, host, port, database):
        self.host = host
        self.port = port
        self.database = database

    def __repr__(self):
        return (
            f"DatabaseConnection("
            f"host={self.host!r}, "
            f"port={self.port!r}, "
            f"database={self.database!r})"
        )

如果涉及密码、Token、身份证号等敏感信息,不要直接放进 __repr__

python 复制代码
class ApiClient:
    def __init__(self, base_url, token):
        self.base_url = base_url
        self.token = token

    def __repr__(self):
        return f"ApiClient(base_url={self.base_url!r}, token='***')"

这是日志安全中的重要细节。


九、实践案例:电商订单对象如何设计?

假设我们正在做一个电商系统,有订单对象:

python 复制代码
class Order:
    def __init__(self, order_id, user_id, amount, status):
        self.order_id = order_id
        self.user_id = user_id
        self.amount = amount
        self.status = status

如果不实现任何字符串方法,日志可能是:

python 复制代码
<__main__.Order object at 0x...>

不利于排查问题。

我们可以这样设计:

python 复制代码
class Order:
    def __init__(self, order_id, user_id, amount, status):
        self.order_id = order_id
        self.user_id = user_id
        self.amount = amount
        self.status = status

    def __repr__(self):
        return (
            f"Order("
            f"order_id={self.order_id!r}, "
            f"user_id={self.user_id!r}, "
            f"amount={self.amount!r}, "
            f"status={self.status!r})"
        )

    def __str__(self):
        return f"订单 {self.order_id},金额 ¥{self.amount:.2f},状态:{self.status}"


order = Order("O20260001", "U1001", 299.9, "paid")

print(repr(order))
print(str(order))

输出:

python 复制代码
Order(order_id='O20260001', user_id='U1001', amount=299.9, status='paid')
订单 O20260001,金额 ¥299.90,状态:paid

这里的设计思路是:

text 复制代码
__repr__ 用于日志、调试、开发者排错
__str__  用于页面展示、CLI 输出、业务文本

这样既满足工程可维护性,又兼顾用户体验。


十、实践案例:数据分析中的对象展示

在数据科学或自动化脚本中,我们经常封装任务结果。

python 复制代码
class AnalysisResult:
    def __init__(self, total_rows, valid_rows, error_rows):
        self.total_rows = total_rows
        self.valid_rows = valid_rows
        self.error_rows = error_rows

    @property
    def success_rate(self):
        if self.total_rows == 0:
            return 0
        return self.valid_rows / self.total_rows

    def __repr__(self):
        return (
            f"AnalysisResult("
            f"total_rows={self.total_rows!r}, "
            f"valid_rows={self.valid_rows!r}, "
            f"error_rows={self.error_rows!r})"
        )

    def __str__(self):
        return (
            f"共处理 {self.total_rows} 行,"
            f"有效 {self.valid_rows} 行,"
            f"异常 {self.error_rows} 行,"
            f"成功率 {self.success_rate:.2%}"
        )


result = AnalysisResult(1000, 963, 37)

print(result)
print(repr(result))

输出:

python 复制代码
共处理 1000 行,有效 963 行,异常 37 行,成功率 96.30%
AnalysisResult(total_rows=1000, valid_rows=963, error_rows=37)

这个例子很适合 Python教程 中介绍面向对象,也很适合真实 Python实战 中封装 ETL、数据校验、日志分析结果。


十一、dataclass 如何自动生成 __repr__

现代 Python 开发中,dataclasses 非常常用。它可以自动帮我们生成 __init____repr__

python 复制代码
from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int
    role: str


user = User("Alice", 18, "admin")

print(user)
print(repr(user))

输出:

python 复制代码
User(name='Alice', age=18, role='admin')
User(name='Alice', age=18, role='admin')

这就是为什么很多业务数据对象推荐使用 @dataclass。它让代码更少,也让调试输出更清晰。

如果某个字段不希望出现在 repr 中,比如密码,可以这样:

python 复制代码
from dataclasses import dataclass, field


@dataclass
class User:
    name: str
    age: int
    password: str = field(repr=False)


user = User("Alice", 18, "secret123")

print(user)

输出:

python 复制代码
User(name='Alice', age=18)

这在处理敏感数据时非常实用。


十二、__repr____str__ 与格式化输出

Python 中有多种把对象转换成字符串的方式:

python 复制代码
str(obj)
repr(obj)
print(obj)
f"{obj}"
f"{obj!r}"
"{}".format(obj)
"{!r}".format(obj)

示例:

python 复制代码
class Book:
    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return f"Book(title={self.title!r})"

    def __str__(self):
        return f"《{self.title}》"


book = Book("流畅的 Python")

print(f"{book}")
print(f"{book!r}")

输出:

python 复制代码
《流畅的 Python》
Book(title='流畅的 Python')

规则是:

text 复制代码
f"{obj}"   使用 str(obj),即 __str__
f"{obj!r}" 使用 repr(obj),即 __repr__

因此,在日志中我经常建议写:

python 复制代码
logger.info("处理订单:%r", order)

或者:

python 复制代码
print(f"调试对象:{order!r}")

这样可以明确使用 __repr__,避免误用面向用户的 __str__


十三、常见错误与避坑指南

错误一:__repr__ 返回的不是字符串

python 复制代码
class User:
    def __repr__(self):
        return 123

调用:

python 复制代码
repr(User())

会报错:

python 复制代码
TypeError: __repr__ returned non-string

__repr____str__ 必须返回字符串。

正确写法:

python 复制代码
class User:
    def __repr__(self):
        return "User()"

错误二:在 __repr__ 中做复杂计算或外部请求

坏例子:

python 复制代码
class User:
    def __repr__(self):
        data = query_database()
        return f"User(data={data})"

这是非常不推荐的。

原因是 repr() 可能在日志、调试器、异常输出、容器展示中被频繁调用。如果 __repr__ 很慢或者有副作用,会让系统变得不可预测。

最佳实践:

text 复制代码
__repr__ 应该快速、稳定、无副作用。

错误三:泄露敏感信息

坏例子:

python 复制代码
class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def __repr__(self):
        return f"User(username={self.username!r}, password={self.password!r})"

如果日志被收集到平台上,密码就泄露了。

更安全的写法:

python 复制代码
class User:
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def __repr__(self):
        return f"User(username={self.username!r}, password='***')"

错误四:__str__ 太技术化

如果 __str__ 是给用户看的,就不要输出过多内部实现细节。

不友好的写法:

python 复制代码
Order(order_id='A001', user_id='U001', amount=199.0, status='paid')

更友好的写法:

python 复制代码
订单 A001 已支付,金额 ¥199.00

十四、操作性总结:项目中应该怎么写?

可以遵循下面这套规则。

1. 业务核心对象优先写 __repr__

例如:

python 复制代码
class Payment:
    def __repr__(self):
        return (
            f"Payment("
            f"id={self.id!r}, "
            f"amount={self.amount!r}, "
            f"status={self.status!r})"
        )

日志排查非常好用。


2. 需要展示给用户时再写 __str__

例如:

python 复制代码
class Payment:
    def __str__(self):
        return f"支付单 {self.id}:¥{self.amount:.2f},状态:{self.status}"

3. 能用 dataclass 就不要手写重复代码

python 复制代码
from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int

它会自动生成清晰的 __repr__


4. 敏感字段必须隐藏

python 复制代码
from dataclasses import dataclass, field


@dataclass
class Token:
    name: str
    value: str = field(repr=False)

5. 日志中优先使用 %r

python 复制代码
logger.info("创建用户成功:%r", user)

这会调用 repr(user),更适合开发者排查问题。


十五、一张图理解二者关系

text 复制代码
对象
 │
 ├── repr(obj)
 │      └── 调用 obj.__repr__()
 │          └── 面向开发者:准确、明确、利于调试
 │
 └── str(obj)
        └── 优先调用 obj.__str__()
            └── 面向用户:友好、自然、适合展示

如果没有 __str__:
str(obj) 会退回使用 __repr__

常见触发场景:

text 复制代码
print(obj)          -> str(obj)
str(obj)            -> __str__
repr(obj)           -> __repr__
f"{obj}"            -> __str__
f"{obj!r}"          -> __repr__
交互式环境直接输入对象 -> __repr__
列表/字典中展示元素    -> 元素的 __repr__
日志调试              -> 推荐使用 __repr__

十六、从更高层看:它们也是 Python 数据模型的一部分

__repr____str__ 并不是孤立技巧,而是 Python 数据模型中的一部分。

Python 的数据模型通过魔术方法定义各种协议:

text 复制代码
__len__     -> 长度协议
__iter__    -> 迭代协议
__enter__   -> 上下文管理协议
__call__    -> 可调用协议
__repr__    -> 开发者字符串表示协议
__str__     -> 用户字符串表示协议

这正是 Python 优雅的地方:语言把很多行为开放给对象自己定义。

你希望对象能被 len() 使用,就实现 __len__

你希望对象能被 for 遍历,就实现 __iter__

你希望对象打印得更清楚,就实现 __repr____str__

这种设计让自定义对象可以像内置类型一样自然地融入 Python 生态。


结语:好的字符串表示,是写给未来自己的温柔

很多人学习 Python 时,会觉得 __repr____str__ 是小知识点。但在真实项目中,它们直接影响调试体验、日志质量、错误排查效率和代码可维护性。

当凌晨线上报警响起,你打开日志,看到的是:

python 复制代码
<__main__.Order object at 0x...>

你会痛苦。

但如果你看到的是:

python 复制代码
Order(order_id='O20260001', user_id='U1001', amount=299.9, status='paid')

你会感谢曾经认真写下 __repr__ 的自己。

所以,最后给出一句实践建议:

核心对象写 __repr__,展示对象写 __str__

__repr__ 要准确安全,__str__ 要友好自然。

Python 编程的魅力,不只在于语法简洁,也在于这些细节能让代码越来越像一门精心打磨的语言。愿你在每一次打印对象、记录日志、排查问题时,都能感受到这种温柔而强大的设计。

欢迎在评论区分享:你有没有因为对象打印不清晰而排查过很久的问题?你在项目里更常重写 __repr__,还是 __str__

相关推荐
枕星而眠1 小时前
C++ 面向对象核心机制深度解析:多态性、虚函数、虚继承与 final 类
运维·开发语言·c++·后端
Sammyyyyy1 小时前
Google I/O 2026 Antigravity 更新解析与 SDK 实战指南
python·ai编程·servbay
Evand J2 小时前
【MATLAB例程】自适应渐消扩展卡尔曼滤波(AFEKF)三维雷达目标跟踪|效果已调优,附下载链接和运行结果,代码直接运行即可
开发语言·算法·matlab·目标跟踪·卡尔曼滤波·自适应滤波·代码定制
爱装代码的小瓶子2 小时前
3. 设计buffer模块
linux·服务器·开发语言·c++·php
郝学胜-神的一滴2 小时前
Qt 高级开发 027: QTabWidget自定义样式表美化实战
开发语言·c++·qt·程序人生·软件构建·用户界面
keykey6.2 小时前
迁移学习实战:用预训练模型做图像分类
开发语言·人工智能·深度学习·机器学习
双河子思2 小时前
《代码整洁之道》——读书笔记(持续更新)
开发语言·c++·c#
嫂子的姐夫2 小时前
047-MD5:飞卢网
爬虫·python·js逆向·逆向
川冰ICE2 小时前
JavaScript实战②|电商网站交互效果,轮播图与购物车
开发语言·javascript·交互