動機
所以你剛剛用 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__
方法。 (有關如何在下面以不同方式使用這兩種方法的更多資訊)。