动机
所以你刚刚用 Python 创建了第一个类,这是一个封装扑克牌的简洁小类:
class Card:
def __init__(self, suit, pips):
self.suit = suit
self.pips = pips
在代码的其他地方,你可以创建此类的一些实例:
ace_of_spades = Card('Spades', 1)
four_of_clubs = Card('Clubs', 4)
six_of_hearts = Card('Hearts', 6)
你甚至创建了一张卡片列表,以表示手:
my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]
现在,在调试过程中,你想看看你的手是什么样的,所以你自然而然地写下来:
`print(my_hand)`
但你得到的是一堆胡言乱语:
[<__main__.Card instance at 0x0000000002533788>,
<__main__.Card instance at 0x00000000025B95C8>,
<__main__.Card instance at 0x00000000025FF508>]
感到困惑,你只想打印一张卡片:
`print(ace_of_spades)`
再次,你得到这个奇怪的输出:
<__main__.Card instance at 0x0000000002533788>
没有恐惧。我们即将解决这个问题。
首先,了解这里发生的事情非常重要。当你写了 print(ace_of_spades)
时,你告诉 Python 你想要打印关于你的代码叫 ace_of_spades
的 Card
实例的信息。公平地说,确实如此。
该输出是由两个重要的位:在 type
对象和对象的 id
。单独的第二部分(十六进制数)足以在 print
调用时唯一地识别对象。[1]
真正发生的是你要求 Python把文字放到那个对象的本质上然后再显示给你。同一机制的更明确的版本可能是:
string_of_card = `str(ace_of_spades)`
`print(string_of_card)`
在第一行中,你尝试将 Card
实例转换为字符串,然后在第二行显示它。
问题
你遇到的问题的出现是由于这样的事实,当你告诉 Python 的一切,它需要了解的 Card
类,以创建卡,你没有告诉它你怎么想 Card
实例转换为字符串。
而且由于它不知道,当你(隐含地)写了 str(ace_of_spades)
时,它给了你所看到的,Card
实例的通用表示。
解决方案(第 1 部分)
但我们可以告诉 Python 我们希望如何将自定义类的实例转换为字符串。我们这样做的方式是使用 __str__``dunder
(用于双下划线)或魔法方法。
每当你告诉 Python 从类实例创建一个字符串时,它将在类上查找 __str__
方法,并调用它。
请考虑以下更新版本的 Card
类:
class Card:
def __init__(self, suit, pips):
self.suit = suit
self.pips = pips
def `__str__(self)`:
special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}
card_name = special_names.get(self.pips, `str(self.pips)`)
return "%s of %s" % (card_name, self.suit)
在这里,我们现在已经在 Card
类上定义了 __str__
方法,在对面部卡进行简单的字典查找之后,返回一个格式化的字符串,但是我们决定。
(注意返回在这里用粗体表示,强调返回字符串的重要性,而不是简单地打印它。打印它似乎有效,但是当你做了像 str(ace_of_spades)
这样的事情时,你会打印出卡片,没有即使在主程序中有一个打印函数调用。所以要清楚,确保 __str__
返回一个字符串。)。
__str__
方法是一种方法,因此第一个参数将是 self
,它既不应该接受,也不应该通过附加参数。
回到我们以更加用户友好的方式显示卡的问题,如果我们再次运行:
ace_of_spades = Card('Spades', 1)
`print(ace_of_spades)`
我们会看到我们的输出更好:
Ace of Spades
太好了,我们已经完成了,对吧?
好吧,为了覆盖我们的基地,让我们仔细检查我们已经解决了我们遇到的第一个问题,打印了 Card
实例列表,hand
。
所以我们重新检查以下代码:
my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]
`print(my_hand)`
而且,令我们惊讶的是,我们再次得到那些有趣的十六进制代码
[<__main__.Card instance at 0x00000000026F95C8>,
<__main__.Card instance at 0x000000000273F4C8>,
<__main__.Card instance at 0x0000000002732E08>]
这是怎么回事?我们告诉 Python 我们希望如何显示我们的 Card
实例,为什么它显然似乎忘了?
解决方案(第 2 部分)
好吧,当 Python 想要获取列表中项目的字符串表示时,幕后机制有点不同。事实证明,Python 并不关心 __str__
。
相反,它会寻找不同的方法,__repr__
,如果多数民众赞成没有找到,倒在了“十六进制的东西。” [2]
所以你说我必须做两种方法来做同样的事情?一个是因为我想把我的卡片单独拿到另一个当它在某种容器中时?
没有,但是首先让我们来看看我们的类,如果我们要实现这两个 __str__
和 __repr__
方法:
class Card:
special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}
def __init__(self, suit, pips):
self.suit = suit
self.pips = pips
def `__str__(self)`:
card_name = Card.special_names.get(self.pips, `str(self.pips)`)
return "%s of %s (S)" % (card_name, self.suit)
def `__repr__(self)`:
card_name = Card.special_names.get(self.pips, `str(self.pips)`)
return "%s of %s (R)" % (card_name, self.suit)
这里,__str__
和 __repr__
两种方法的实现完全相同,只是为了区分这两种方法,(S)
被添加到 __str__
返回的字符串中,(R)
被添加到 __repr__
返回的字符串中。
请注意,就像我们的 __str__
方法一样,__repr__
不接受任何参数并返回一个字符串。
我们现在可以看到对每种情况负责的方法:
ace_of_spades = Card('Spades', 1)
four_of_clubs = Card('Clubs', 4)
six_of_hearts = Card('Hearts', 6)
my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]
`print(my_hand)` # [Ace of Spades (R), 4 of Clubs (R), 6 of Hearts (R)]
`print(ace_of_spades)` # Ace of Spades (S)
正如所涵盖的那样,当我们将 Card
实例传递给 print
时调用了 __str__
方法,当我们将实例列表传递给 print
时调用了 __repr__
方法。
在这一点上,值得指出的是,正如我们可以使用 str()
从自定义类实例显式创建一个字符串,我们也可以使用内置函数 repr()
显式创建类的字符串表示。
例如:
str_card = `str(four_of_clubs)`
`print(str_card)` # 4 of Clubs (S)
repr_card = `repr(four_of_clubs)`
`print(repr_card)` # 4 of Clubs (R)
另外,如果定义了,我们可以直接调用这些方法(虽然看起来有点不清楚和不必要):
print(`four_of_clubs.__str__()`) # 4 of Clubs (S)
print(`four_of_clubs.__repr__()`) # 4 of Clubs (R)
关于那些重复的功能……
Python 开发人员意识到,如果你想从 str()
和 repr()
返回相同的字符串,你可能需要在功能上复制方法 - 没人喜欢。
因此,有一种机制可以消除对此的需求。一个我偷偷摸摸你到目前为止。事实证明,如果一个类实现了 __repr__
方法而不是 __str__
方法,并且你将该类的实例传递给 str()
(无论是隐式还是显式),Python 将回退到你的 __repr__
实现并使用它。
因此,需要明确的是,请考虑以下版本的 Card
类:
class Card:
special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}
def __init__(self, suit, pips):
self.suit = suit
self.pips = pips
def `__repr__(self)`:
card_name = Card.special_names.get(self.pips, `str(self.pips)`)
return "%s of %s" % (card_name, self.suit)
请注意,此版本仅实现 __repr__
方法。尽管如此,调用 str()
会产生用户友好的版本:
`print(six_of_hearts)` # 6 of Hearts (implicit conversion)
print(`str(six_of_hearts)`) # 6 of Hearts (explicit conversion)
和 repr()
一样:
print([six_of_hearts]) #[6 of Hearts] (implicit conversion)
print(`repr(six_of_hearts)`) # 6 of Hearts (explicit conversion)
摘要
为了让你的类实例能够以用户友好的方式展示自己,你将需要考虑至少实现你的类的 __repr__
方法。如果内存服务,在谈话中 Raymond Hettinger 说确保类实现 __repr__
是他在进行 Python 代码审查时首先要做的事情之一,到现在为止应该清楚原因。你的信息量可以相比微不足道的时候,往往不那么有用(类型,ID),其默认情况下提供的信息添加到调试语句,崩溃报告,或日志文件用一个简单的方法是压倒性的。
如果你想要在容器内部进行不同的表示,则需要实现 __repr__
和 __str__
方法。 (有关如何在下面以不同方式使用这两种方法的更多信息)。