注:原書作者 Steven F. Lott,原書名為 Mastering Object-oriented Python
有許多特殊方法允許類與Python緊密結合,標準庫參考將其稱之為基本,基礎或本質可能是更好的術語。這些特殊方法構成了建立與其他Python特性無縫整合的類的基礎。
例如,對於給定物件的值,我們需要字串表示。基類、物件都有預設的__repr__()
和__str__()
用於提供物件的字串表示。遺憾的是,這些預設表示不提供資訊。我們總是想要覆蓋這些預設定義中的一個或兩個。我們可以看看__format__()
,這個更復雜但目的和前面是一樣的。
我們也可以看看其他轉換,特別是__hash__()
、__bool__()
、和__bytes__()
。這些方法將一個物件轉換成數字、true/false
值或字串的位元組。例如,當我們實現__bool__()
時,可以在if
語句中使用物件,如下:if someobject:
。
然後,我們可以看看實現比較操作符的特殊方法__lt__()
、__le__()
、__eq__()
、__ne__()
、__gt__()
和__ge__()
。
這些基本的特殊方法在類中定義中幾乎總是需要的。
最後我們來看看__new__()
和__del__()
,因為這些方法的用例相當複雜。每當我們需要其他基本特殊方法時,是不需要這些的。
我們會詳細看看如何新增這些特殊方法來擴大一個簡單的類定義。我們需要看一下兩個從物件繼承的預設行為,這樣我們可以瞭解需要什麼樣的覆蓋以及何時真正需要。
__repr__()
和 __str__()
方法
對於一個物件,Python有兩種字串表示方法。這些都和內建函式__repr__()
、__str__()
、__print__()
以及string.format()
方法緊密結合。
str()
方法表示的物件通常是適用於人理解的,由物件的__str__()
方法建立。repr()
方法表示的物件通常是適用於直譯器理解的,可能是完整的Python表示式來重建物件。文件中是這樣說的:對於許多型別,這個函式試圖返回一個字串,將該字串傳遞給eval()
會重新生成物件。
這是由物件的__repr__()
方法建立的。print()
函式會使用str()
準備物件用於列印。- 字串的
format()
方法也可以訪問這些方法。當我們使用{!r}
或{!s}
格式,我們分別需要__repr__()
或__str__()
。
首先讓我們看看預設實現。
下面是一個簡單的類層次結構:
1 2 3 4 5 6 7 8 9 10 |
pythonclass Card: insure = False def __init__(self, rank, suit): self.suit = suit self.rank = rank self.hard, self.soft = self._points() class NumberCard(Card): def _points(self): return int(self.rank), int(self.rank) |
我們已經定義了帶有四個屬性的兩個簡單類。
以下是一個與其中一個類物件的互動:
1 2 3 4 5 6 7 |
>>> x=NumberCard( '2', '♣') >>> str(x) '<__main__.NumberCard object at 0x1013ea610>' >>> repr(x) '<__main__.NumberCard object at 0x1013ea610>' >>> print(x) <__main__.NumberCard object at 0x1013ea610> |
從這個輸出知道預設的__str__()
和__repr__()
實現不是很豐富。
當我們需要覆蓋__str__()
和__repr__()
時我們考慮下面兩個廣泛的設計用例:
- 非集合物件:一個不包含其他物件集合的“簡單”物件,通常不涉及非常複雜格式的集合。
- 集合物件:一個包含一組更復雜格式的物件。
1. 非集合__str__()
和__repr__()
正如我們之前看到的,__str__()
和__repr__()
的輸出不是很豐富。我們幾乎總是需要覆蓋它們。以下是當沒有包含集合的時候覆蓋__str__()
和__repr__()
的方法。這些方法從屬於之前定義的Card
類:
1 2 3 4 5 |
pythondef __repr__(self): return "{__class__.__name__}(suit={suit!r}, rank={rank!r})".format( __class__=self.__class__, **self.__dict__) def __str__(self): return "{rank}{suit}".format(**self.__dict__) |
這兩個方法依賴於傳遞內部物件的例項變數__dict__()
給format()
函式。對於物件使用__slots__
是不適合的;通常,這些是不可變的物件。在格式說明符中使用名稱會使得格式化更顯式。當然也使得格式模板更長了。在__repr__()
中,我們傳遞內部__dict__
加上物件的__class__
作為關鍵字引數值給format()
函式。
模板字串使用兩種格式說明符:
{__class__.__name__}
模板也可以寫成{__class__.__name__!s}
從而當只提供簡單字串的類名時變得更顯式。{suit!r}
和{rank!r }
模板都使用!r
格式說明符生成屬性值的repr()
方法。
在__str__()
中,我們只有傳遞內部__dict__
物件。格式化使用隱式的{!s}
格式說明符來生成屬性值的str()
方法。
2. 集合__str__()
和__repr__()
當包含一個集合時,我們需要格式化集合中的每個專案以及整個容器。以下是帶有__str__()
和__repr__()
方法的簡單集合:
1 2 3 4 5 6 7 8 9 10 |
pythonclass Hand: def __init__(self, dealer_card, *cards): self.dealer_card = dealer_card self.cards = list(cards) def __str__(self): return ", ".join(map(str, self.cards)) def __repr__(self): return "{__class__.__name__}({dealer_card!r}, {_cards_str})".format( __class__=self.__class__, _cards_str=", ".join( map(repr, self.cards)),**self.__dict__) |
__str__()
方法是一個簡單的設計,如下:
- 對映
str()
到集合中的每一項。這將在生成的每個字串值上建立一個迭代器。 - 使用
", ".join()
合併所有項的字串到一個長字串中。
__repr__()
方法是一個多部分的設計,如下:
- 對映
repr()
到集合中的每一項。這將在生成的每個字串值上建立一個迭代器。 - 使用
", ".join()
合併所有項的字串。 - 建立一組帶有
__class__
的關鍵字、集合字串和來自__dict__
的各種屬性。我們已經命名集合字串為_card_str
,與現有的屬性不衝突。 - 使用
"{__class__.__name__}({dealer_card!r}, {_card_str})".format()
將類名與專案值的長字串結合。我們使用!r
進行格式化以確保屬性也使用了repr()
轉換。
在某些情況下,這可以使一些事情變得稍微簡單些。位置引數的格式化可以一定程度上縮短模板字串的長度。
__format__()
方法
和內建函式format()
一樣,string.format()
使用__format__()
方法。這兩個介面都是用來從給定物件得到像樣的字串的方式。
以下是將引數提供給__format__()
的兩種方法:
someobject.__format__("")
:發生在應用程式使用format(someobject)
或等價的"{0}".format(someobject)
的時候。在這些情況下,提供了長度為零的字串說明符。這會產生一個預設格式。someobject.__format__(specification)
:發生在應用程式使用format(someobject, specification)
或等價的"{0:specification}".format(someobject)
的時候。
請注意,類似於"{0!r}".format()
或"{0!s}".format()
的方法沒有使用__format__()
方法。它們直接使用了__repr__()
或__str__()
。
帶有""
說明符的合理響應是返回str(self)
。它對各種物件的字串提供了顯式的一致性表示。
格式說明符必須都是文字,且在格式字串的":"
之後。當我們寫"{0:06.4f}"
,06.4f
是適用於第0項引數列表的格式說明符。
Python標準庫文件中的6.1.3.1節定義了一個複雜的數值說明符作為九個部分字串,這是格式說明符的一種迷你語言。有如下語法:
1 |
[[fill]align][sign][#][0][width][,][.precision][type] |
我們可以用正規表示式解析這些標準說明符,如下面程式碼片段所示:
1 2 3 4 5 6 7 8 9 |
re.compile( r"(?P<fill_align>.?[\<\>=\^])?" "(?P<sign>[-+ ])?" "(?P<alt>#)?" "(?P<padding>0)?" "(?P<width>\d*)" "(?P<comma>,)?" "(?P<precision>\.\d*)?" "(?P<type>[bcdeEfFgGnosxX%])?" ) |
這個正則將說明符拆分成八組。第一組和原說明符一樣有fill
和alignment
欄位。我們可以使用這些得出我們已定義類的格式化數值資料。
然而,Python的格式說明符迷你語言可能不適用於我們的類定義。因此,我們需要定義我們自己的說明符迷你語言並在類的__format__
方法中執行。如果我們定義數值型別,我們應該堅持預定義的迷你語言。然而,對於其他型別則沒有理由再堅持預定義的語言。
作為一個示例,這裡有個微不足道的語言,使用字元%r
和%s
分別給我們展示牌值和花色。在結果字串中%%
字元變成%
。所有其他字元是重複的。
我們可以通過格式化擴充套件我們的Card
類,如下面程式碼片段所示:
1 2 3 4 5 6 |
pythondef __format__(self, format_spec): if format_spec == "": return str(self) rs = format_spec.replace("%r", self.rank).replace("%s", self.suit) rs = rs.replace("%%", "%") return rs |
這個定義會檢查格式說明符。如果沒有說明符,則使用str()
函式。如果提供了一個說明符,會合攏牌值、花色和任何%
字元格式說明符,將其轉化為輸出字串。
這允許我們像下面這樣格式化Card
:
1 |
pythonprint( "Dealer Has {0:%r of %s}".format(hand.dealer_card)) |
格式說明符("%r of %s")
被作為format
的引數傳遞給我們的__format__()
方法。使用這個,我們能夠提供一個一致的介面來表示我們已經定義的類的物件。
或者,我們可以定義如下:
1 2 3 4 5 6 7 |
pythondefault_format = "some specification" def __str__(self): return self.__format__(self.default_format) def __format__(self, format_spec): if format_spec == "": format_spec = self.default_format # process the format specification. |
這個的優勢在於把所有字串放置到__format__()
方法,而不是分開到的__format__()
和__str__()
。劣勢在於,我們不總是需要實現__format__()
,但我們幾乎總是需要實現__str__()
。
1. 巢狀格式化說明符
string.format()
方法可以處理巢狀的{}
例項來執行簡單的關鍵字置換到格式說明符中。這個置換完成,會建立最終格式字串並傳遞給類的__format__()
方法。這種巢狀置換通過引數化通用說明符簡化了某些相對複雜的數值格式。
下面的例子,我們可以在format
引數中很容易的修改width
:
1 2 3 |
pythonwidth = 6 for hand, count in statistics.items(): print( "{hand} {count:{width}d}".format(hand=hand, count=count, width=width)) |
我們定義了一個通用的格式,"{hand:%r%s } {count:{width}d}"
,這需要一個width
引數讓它變成適用的格式說明符。
為format()
方法提供width=
引數的值被用於替代{width}
巢狀說明符。一旦被替換,最終格式會作為一個整體提供給__format__()
方法。
2. 集合與委託格式說明符
格式化一個包括集合的複雜物件,有兩個格式化問題:如何格式化整體物件以及如何格式化集合中的專案。當我們看到Hand
,例如,我們看到我們有單獨的Card
類集合。我們需要Hand
委託格式化細節給單獨的Card
例項。
下面是一個適用於Hand
的__format__()
方法:
1 2 3 4 |
pythondef __format__(self, format_specification): if format_specification == "": return str(self) return ", ".join("{0:{fs}}".format(c, fs=format_specification) for c in self.cards) |
format_specification
引數將用於每個Hand
集合裡面的Card
例項。格式說明符"{0:{fs}}"
使用巢狀格式說明符技術將format_specification
字串置入到應用於Card
例項的建立。使用這種方法我們可以格式化Hand
物件、player_hand
,如下所示:
1 |
python"Player: {hand:%r%s}".format(hand=player_hand) |
這將應用%r%s
格式說明符到Hand
物件中的Card
例項。