原文链接:https://realpython.com/python-repr-vs-str/
by Stephen Gruppetta Mar 22, 2023
目录
- 简短地说:
.__repr__()
给程序员看而.__str__()
给用户看- 如何获取一个对象的字符串表示?
- 你应该在自定义类里定义
.__repr__()
和.__str__()
吗?- 结语
电脑程序执行的最常见的任务之一就是展示数据。程序通常把信息展示给使用者看。然而,一个程序也应该把信息展示给程序员看,以便开发和维护。程序员需要的关于对象的信息跟展示给用户的应该有所不同,这就是.__repr__()
vs .__str__()
的由来。
一个Python对象有多个提供特定行为的特殊方法。有两个相似的特殊方法都是以字符串形式来描述对象。这就是 .__repr__()
和 .__str__()
。 .__repr__()
方法返回一则程序员在维护和debug时所需的详细说明。而 .__str__()
返回一则简短信息给用户。
.__repr__()
和 .__str__()
方法是你能在任何类里定义的特殊方法。让你能在一些常见的输出方式里控制对象的展示方式,例如用 print()
函数获取到的结果,格式化字符串和交互环境。
在本教程中,你将学到如何区分 .__repr__()
和 .__str__()
以及如何在定义类时使用这些特殊方法。有效地定义这些方法会使你编写的类可读性更强、更容易debug和维护。所以,你什么时候该选择.__repr__()
或.__str__()
?
Free Download: Get a sample chapter from Python Tricks: The Book that shows you Python’s best practices with simple examples you can apply instantly to write more beautiful + Pythonic code.
简短地说: .__repr__()
给程序员看而 .__str__()
给用户看
Python classes 有一些 special methods (特殊方法)。这些方法的名称以双下划线开头和结尾。你也可以通俗地称之为 dunder method
(双下划线方法)因为它们的名字里有双下划线。
.__repr__()
和 .__str__()
特殊方法都返回对象的字符串表示。字符串表示是一个展示了对象信息的字符串。你可以为不同的受众定制信息,例如用户或你的程序员小伙伴。
之所以有2个方法来展示一个对象,是因为它们的用处不同:
.__repr__()
提供了一个对象的正式字符串表示,目的是给程序员看。.__str__()
提供了一个对象的非正式字符串表示,目的是给用户看。
.__repr__()
返回的字符串表示用来供程序员开发、维护程序。通常来说,它提供了这个对象的详细的、无歧义的信息。另一个正式字符串表示的重要性质是,一个程序员应该可以比照它来创建一个跟原始对象一样的新对象。
.__str__()
方法提供了面向用户的字符串表示,这个用户不一定要是Python程序员。因此,这种表示得确保任何用户都能理解对象里蕴含的信息。一般地, .__str__()
更简单、更易阅读。
**注意:**这里有关于这两个方法的一场讨论,The Real Python Podcast: Episode 153。
展示对象两种表示的一个方式是使用 Python’s standard REPL (Python的标准解释器)。当你运行一行只有一个对象的代码时,解释器会展示 .__repr__()
返回的字符串表示。与之相对,内置的 print()
函数展示了 .__str__()
返回的非正式字符串表示。
你可以看看 datetime
模块里的 datetime
类的对象的 .__repr__()
和 .__str__()
返回的字符串:
>>> import datetime
>>> today = datetime.datetime.now()
>>> today
datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)
>>> print(today)
2023-02-18 18:40:02.160890
你使用 .now()
创建了一个名叫 today
的 datetime.datetime
对象。这个方法返回当前日期和时间。当你对仅包含变量 today
的一行代码求值时,解释器会展示由 .__repr__()
返回的字符串表示。这种表示展现了数据类型的名字以及重新创建这样一个对象所需的参数。
当你使用 print()
,解释器会展示 .__str__()
返回的表示形式。这条字符串展示了 ISO standard format (国际标准格式)的日期和时间。因此,这不是Python里的特定格式,而是广泛用来表示日期、时间的一个标准格式。
通常来讲,正式的字符串表示形式是一个有效的Python语句,你可以用同样的值来创建新的对象。通过复制 datetime.datetime
对象的正式字符串表示形式并赋值给一个新的变量,就能确认这一点。你也可以试试复制非正式的字符串表示形式,但不会起作用的:
>>> new_date = datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)
>>> new_date == today
True
>>> new_date = 2023-02-18 18:40:02.160890
Traceback (most recent call last):
...
File "<input>", line 1
new_date = 2023-02-18 18:40:02.160890
^
SyntaxError: leading zeros in decimal integer literals are not permitted ...
你对 today
求值时,从解释器那得到输出能创建出一个跟原来对象一样的新对象。
然而, 你用 print()
得到的 .__str__()
的字符串表示不是有效的Python表达式,所以报了 SyntaxError
。
你也可以看看内置数据类型的字符串表示形式:
>>> 5
5
>>> print(5)
5
>>> greeting = "How are you?"
>>> greeting
'How are you?'
>>> print(greeting)
How are you?
如果你对一行仅有一个对象或变量名的代码求值时,Python解释器就返回正式字符串表示。但是用 print()
时又发生了什么呢?在上面的例子里,正式的、非正式的字符串表示形式一致或非常相近。两种表示形式都基本由你创建对象时使用的 literals (字面量)决定。
其他内置数据类型也有相同的正式、非正式字符串表示形式。在这些情况里,两种表示形式都跟创建对象时的字面量相同。
**注意:**虽然大多数内置数据类型的两种字符串表示形式都与字面量保持一致,但也有例外。例如,
set()
是空集合的正式与非正式字符串形式,因为字面量{}
代表的是空字典。
在下面这个例子中,你可以看看列表和字典的表示形式:
>>> [10, 20, 30]
[10, 20, 30]
>>> print([10, 20, 30])
[10, 20, 30]
>>> {"name": "Homer", "role": "author"}
{'name': 'Homer', 'role': 'author'}
>>> print({"name": "Homer", "role": "author"})
{'name': 'Homer', 'role': 'author'}
这些表示形式都没有歧义,并且可以用同样的值来创建新的对象。而且,它们对程序使用者也足够清晰,因为它们传达了对象里的信息,也无法进一步简化了。这些对象不需要针对程序员和用户具有不同的字符串表达。
**注意:**你可以在Python里使用 pretty printing 来把你的对象格式化成更方便阅读的样式,这在debug时很有用。
在这个章节,你学到了由 .__repr__()
返回的正式字符串表示形式应该包含对象的无歧义信息,而且是面向程序员。你通常可以用这种表示形式来创建一个跟原始对象相同的对象。
与之相对, .__str__()
返回的非正式字符串表示形式面向用户,而且通常更简单。大部分内置数据类型的字面量就是它的正式和非正式字符串表示。
在下一个章节,你会学到其他获取这两种字符串表示形式的方法。
如何获取一个对象的字符串表示?
你已经看过了如何在标准Python解释器里展示两种字符串表示。到目前为止,你已经使用了特殊方法来做到这一点。但通常来讲,你也可以用内置的 repr()
函数和 str()
函数来获取正式的和非正式的字符串表示。
当你把一个对象传给 repr()
或 str()
,背后就是在调用这个对象的 .__repr__()
或 .__str__()
方法。你可以通过先前章节创建的 datetime.datetime
对象来确认这点:
>>> import datetime
>>> today = datetime.datetime.now()
>>> repr(today)
'datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)'
>>> today.__repr__()
'datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)'
>>> str(today)
'2023-02-18 18:40:02.160890'
>>> today.__str__()
'2023-02-18 18:40:02.160890'
当你把 today
传给 内置函数repr
,程序会调用 today.__repr__()
。虽然你可以直接调用特殊方法 today.__repr__()
,但最好还是使用内置函数 repr()
,因为在任何情况下你都不应该直接访问特殊方法。特殊方法的目的是给对象增加功能,而不是直接调用。
类似地,你也可以用 str()
。它会调用传入参数的 .__str__()
方法。
**注意:**表面上来看,用
str()
跟用repr()
确实差不多。然而,repr()
是一个内置函数,而str
是一个类。因此,str()
通过把传入参数转为字符串创建了一个str
类的实例。这种差异并不会影响你使用str()
和repr()
的方式,基于它们都是 callable 的事实,你的用法跟它们是个函数还是类的构造器无关。
你必须将一个对象作为参数传给 str()
和 repr()
。然而,.__repr__()
和 .__str__()
是不需要传额外参数的方法。和所有的 instance methods (实例方法)一样,对象自己会作为第一个参数传给方法。对于 .__repr__()
和 .__str__()
,对象也是唯一参数,并且这些方法不需要另外的参数。
你也可以使用内置的 format()
函数来展示一个对象的字符串表示,用字符串方法 .format()
, 或是 f-strings 也可以。你可以先试试把 datetime
对象 today
传给 format()
函数和 .format()
方法:
>>> format(today)
'2023-02-18 18:40:02.160890'
>>> "{}".format(today)
'2023-02-18 18:40:02.160890'
默认情况下,这些返回 .__str__()
返回的对象的非正式字符串表示。在接下来的章节里学习f-strings时,你会看到如何覆盖默认的表示形式。
你在格式化字符串时最有可能使用的就是f-strings了,因为它们是Python里最新的字符串格式方式。你可以通过使用f-strings展示 today
类:
>>> f"{today}"
'2023-02-18 18:40:02.160890'
就跟 format()
和 .format()
一样,f-strings 也展示了 .__str__()
返回的非正式字符串表示。你可以通过在f-string里使用 string conversion flag (字符串转换标识)"!r"来得到正式的字符串表示:
>>> f"{today!r}"
'datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)'
转换标识"!r"覆写了默认的f-string,调用了对象的 .__repr__()
方法。
你也可以在f-string里使用等号(=)来同时展示变量名和值,主要在debug时这么做:
>>> f"{today = }"
'today = datetime.datetime(2023, 2, 18, 18, 40, 2, 160890)'
注意当你使用等号时,f-string默认使用 .__repr__()
返回的正式字符串表示。在这种情况下,由于在f-string里用等号通常都是在debug时使用,所以这种表示是面向程序员的。
你也可以通过使用转换标识"!s"来覆写这种行为:
>>> f"{today = !s}"
'today = 2023-02-18 18:40:02.160890'
当你使用"!s"转换标识,f-string会使用非正式字符串表示。
在下一个章节,你会学习往自定义的类中加入 .__repr__()
和 .__str__()
。
你应该在自定义类里定义 .__repr__()
和 .__str__()
吗?
当你定义一个类,你可以定义一些特殊方法,以便给它增加功能。关于如何定义类,你在这篇教程里能学到更多: Object-Oriented Programming (OOP) in Python 3 。
你可以通过定义一个 Book
类来学习自定义类的字符串表示。你可以用一个 book.py
脚本来定义这个类:
# book.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
odyssey = Book("The Odyssey", "Homer")
print(odyssey)
你定义的类 Book
有一个 .__init__()
方法。初始化时需要2个参数, title
和 author
。
你带着标题和作者创建了 The Odyssey 这本书的实例,然后你把 odyssey
传给 print()
函数。你使用 print()
是因为当你在脚本里对一行只有变量名的代码求值时,没有任何输出。让你运行这个脚本,会得到下面的输出:
$ python book.py
<__main__.Book object at 0x1025c4ed0>
这个输出就是继承自 object
类的对象的默认字符串表示。object
类是所有Python类的基类。它展示了:
__main__.Book
: 类名以及定义位置0x1025c4ed0
: 对象的内存地址
默认的字符串表示告诉了你类名和内存地址,以一个hexadecimal value (十六进制值)展示。在CPython里,内存地址就是对象的标识。你可以使用 built-in function id()
(内置函数id()
)来获取对象的标识,此时返回十进制数而不是十六进制数。
然而,内存地址鲜有用处。默认的表示形式并不提供任何对用户或程序员有帮助的额外对象信息。
repr()
和 str()
都会返回这种默认的字符串表示:
# book.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
odyssey = Book("The Odyssey", "Homer")
print(repr(odyssey))
print(str(odyssey))
你调用 repr(odyssey)
和 str(odyssey)
然后打印出它们的返回值。输出都是默认的表示形式:
$ python book.py
<__main__.Book object at 0x100d046d0>
<__main__.Book object at 0x100d046d0>
目前,输出只能获取到默认的表示形式。
你可以给这个类定义 .__repr__()
特殊方法:
# book.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __repr__(self):
class_name = type(self).__name__
return f"{class_name}(title={self.title!r}, author={self.author!r})"
odyssey = Book("The Odyssey", "Homer")
print(repr(odyssey))
print(str(odyssey))
.__repr__()
方法除了 self
以外没有别的参数,并且必须返回一个字符串。这段代码的输出展示了由 repr(odyssey)
和 str(odyssey)
返回的字符串表示,这种表示是你刚定义的:
$ python book.py
Book(title='The Odyssey', author='Homer')
Book(title='The Odyssey', author='Homer')
repr()
内置函数调用了对象的 .__repr__()
方法。如果一个类没有定义 .__str__()
方法,那么 str()
默认也会调用 .__repr__()
方法。
你定义了 .__repr__()
的返回值,包括类名、紧跟在后面的括号和两个用于初始化类的参数。这种格式是一种理想的正式字符串表示,因为它是一段有效的Python表达式,能创建出和原对象一样的对象。不论何时,你都应该用这种格式来定义正式字符串表示。
下一步,你可以为这个类定义 .__str__
方法:
# book.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __repr__(self):
class_name = type(self).__name__
return f"{class_name}(title={self.title!r}, author={self.author!r})"
def __str__(self):
return self.title
odyssey = Book("The Odyssey", "Homer")
print(repr(odyssey))
print(str(odyssey))
你定义了 .__str__()
特殊方法,除了 self
外没有任何额外参数,并且应该返回一个字符串。这种表示形式可以是任何你认为能帮助到用户的字符串。这个版本的代码输出顺序展现了正式和非正式的字符串表示形式:
$ python book.py
Book(title='The Odyssey', author='Homer')
The Odyssey
.__str__()
返回的非正式字符串表示仅仅展示出了书名,也没有对类名的引用。你可以自定义非正式字符串表示来满足你的需求:
# book.py
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __repr__(self):
class_name = type(self).__name__
return f"{class_name}(title={self.title!r}, author={self.author!r})"
def __str__(self):
return f'"{self.title}" by {self.author}'
odyssey = Book("The Odyssey", "Homer")
print(repr(odyssey))
print(str(odyssey))
现在,.__str__()
返回的字符串是用引号括起来的书名和紧随其后的作者名。注意如果你想在字符串内部用双引号,可以用单引号表示字符串。这段代码的输出先是展示了正式的字符串表示,然后就是修改后的非正式字符串表示:
$ python book.py
Book(title='The Odyssey', author='Homer')
"The Odyssey" by Homer
正式的字符串表示包含了一个程序员需要的所有细节,并允许你复制对象进行深入研究。这么做有2个主要好处:
- 使程序更易维护
- 有助于debug
你可以使用这个字符串表示里的表达式来创建一个跟原来一样的对象。这种特性在debug时很有用,因为你或者你的小伙伴可以复制一个对象来深入研究。
然而,非正式字符串表示是一种对用户来说更合适、更友好的格式。这种表示对他们来说更容易阅读。
**注意:**当实现这两个特殊方法时,应该采取额外的防范措施,避免泄露敏感信息,例如用户密码是无论如何都不能暴露出来的。最好跳过这种属性,或是加密,然后再返回相关字符串。
当你定义了一个类,最好要定义 .__repr__()
,这样(你的类)就有了一个正式的字符串表示。用过实现这个方法,你避开了用处不大的默认字符串表示形式。这个方法也是非正式字符串表示的备选方案,如果你的两种表示形式本来就是一样的,就显得很方便。
如果你需要把对象展示给用户,可以定义 .__str__()
。这个方法的输出对用户来讲更容易理解。
如果你在用Python的 data classes ,它里面已经包含了默认的正式字符串表示。你不需要再自己去定义 .__repr__()
,除非你想覆盖默认格式。
现在你已经准备好在任何自定义的类中实现字符串表示了。
结语
在本篇教程中,你学到了Python对象中正式、非正式字符串表示的区别。特殊方法 .__repr__()
返回正式字符串表示,供程序员开发和维护程序。而特殊方法 .__str__()
返回的非正式字符串表示,则对用户来说更加友好。
现在,你可以区分这两种表示,也知道在哪种情况下使用哪种表示。如果你还没有这么做,那么至少应该在类里实现 .__repr__()
方法来提供正式字符串表示。
Free Download: Get a sample chapter from Python Tricks: The Book that shows you Python’s best practices with simple examples you can apply instantly to write more beautiful + Pythonic code.