Python 自定義類中的函數(shù)和運(yùn)算符重載

原文:https://realpython.com/operator-function-overloading/

如果你曾在字符串(str)對象上進(jìn)行過 + 或 * 運(yùn)算闻牡,你一定注意到它跟整數(shù)或浮點(diǎn)數(shù)對象的行為差異:

>>> # 加法
>>> 1 + 2
3

>>> # 拼接字符串
>>> 'Real' + 'Python'
'RealPython'


>>> # 乘法
>>> 3 * 2
6

>>> # 重復(fù)字符串
>>> 'Python' * 3
'PythonPythonPython'

你可能想知道税朴,為什么同一個(gè)內(nèi)置的操作符或函數(shù)寻狂,作用在不同類的對象上面會(huì)展現(xiàn)出不同的行為双妨。這種現(xiàn)象被稱為運(yùn)算符重載或者函數(shù)重載。本文將幫助你理解這個(gè)機(jī)制往枣,今后你可以運(yùn)用到你的自定義類中肛根,讓你的編碼更 Pythonic 。

以下你將學(xué)到:

  • Python 處理運(yùn)算符和內(nèi)置函數(shù)的API
  • len() 以及其它內(nèi)置函數(shù)背后的“秘密”
  • 如何讓你的類可以使用運(yùn)算符進(jìn)行運(yùn)算
  • 如何讓你的類與內(nèi)置函數(shù)的操作保持兼容及行為一致

此外活鹰,后面還將提供一個(gè)具體的類的實(shí)例哈恰。它的實(shí)例對象的行為與運(yùn)算符及內(nèi)置函數(shù)的行為保持一致只估。

Python 數(shù)據(jù)模型

假設(shè),你有一個(gè)用來表示在線購物車的類着绷,包含一個(gè)購物車(列表)和一名顧客(字符串或者其它表示顧客類的實(shí)例)蛔钙。

這種情形下,很自然地需要獲取購物車的列表長度荠医。Python 的新手可能會(huì)考慮在他的類中實(shí)現(xiàn)一個(gè)叫 get_cart_len() 的方法來處理這個(gè)需求吁脱。實(shí)際上,你只需要配置一下彬向,當(dāng)我們傳入購物車實(shí)例對象時(shí)兼贡,使用內(nèi)置函數(shù) len() 就可以返回購物車的長度。

另一個(gè)場景中幢泼,我們可能需要添加某些商品到購物車紧显。某些新手同樣會(huì)想要實(shí)現(xiàn)一個(gè)叫 append_to_cart() 的方法來處理獲取一個(gè)項(xiàng),并將它添加到購物車列表中缕棵。其實(shí)你只需配置一下 + 運(yùn)算符就可以實(shí)現(xiàn)將項(xiàng)目添加到購物車列表的操作孵班。

Python 使用特定的方法來處理這些過程。這些特殊的方法都有一個(gè)特定的命名約定招驴,以雙下劃線開始篙程,后面跟命名標(biāo)識符,最后以雙下劃線結(jié)束别厘。

本質(zhì)上講虱饿,每一種內(nèi)置的函數(shù)或運(yùn)算符都對應(yīng)著對象的特定方法。比如触趴,len()方法對應(yīng)內(nèi)置 len() 函數(shù)氮发,而 add() 方法對應(yīng) + 運(yùn)算符。

默認(rèn)情況下冗懦,絕大多數(shù)內(nèi)置函數(shù)和運(yùn)算符不會(huì)在你的類中工作爽冕。你需要在類定義中自己實(shí)現(xiàn)對應(yīng)的特定方法,實(shí)例對象的行為才會(huì)和內(nèi)置函數(shù)和運(yùn)算符行為保持一致披蕉。當(dāng)你完成這個(gè)過程颈畸,內(nèi)置函數(shù)或運(yùn)算符的操作才會(huì)如預(yù)期一樣工作

這些正是數(shù)據(jù)模型幫你完成的過程(文檔的第3部分)。該文檔中列舉了所有可用的特定方法没讲,并提供了重載它們的方法以便你在自己的對象中使用眯娱。

我們看看這意味著什么。

趣事:由于這些方法的特殊命名方式爬凑,它們又被稱作 dunder 方法徙缴,是雙下劃線方法的簡稱。有時(shí)候它們也被稱作特殊方法或魔術(shù)方法嘁信。我們更喜歡 dunder 方法這個(gè)叫法娜搂。

len() 和 [] 的內(nèi)部運(yùn)行機(jī)制

每一個(gè) Python 類都為內(nèi)置函數(shù)或運(yùn)算符定義了自己的行為方式迁霎。當(dāng)你將某個(gè)實(shí)例對象傳入內(nèi)置函數(shù)或使用運(yùn)算符時(shí)吱抚,實(shí)際上等同于調(diào)用帶相應(yīng)參數(shù)的特定方法百宇。

如果有一個(gè)內(nèi)置函數(shù),func()秘豹,它關(guān)聯(lián)的特定方法是 func()携御,Python 解釋器解釋為類似于 obj.func() 的函數(shù)調(diào)用,obj 就是實(shí)例對象既绕。如果是運(yùn)算符操作啄刹,比如 opr ,關(guān)聯(lián)的特定方法是 opr()凄贩,Python 將 obj1 <opr> obj2 解釋為類似于 obj1.opr(obj2) 的形式誓军。
所以,當(dāng)你在實(shí)例對象上調(diào)用 len() 時(shí)疲扎,Python 將它處理為 obj.len() 調(diào)用昵时。當(dāng)你在可迭代對象上使用 [] 運(yùn)算符來獲取指定索引位置上的值時(shí),Python 將它處理為 itr.getitem(index)椒丧,itr 表示可迭代對象壹甥,index 表示你要索引的位置。

因此壶熏,在你定義自己的類時(shí)句柠,你可以重寫關(guān)聯(lián)的函數(shù)或運(yùn)算符的行為。因?yàn)榘艏伲琍ython 在后臺調(diào)用的是你定義的方法溯职。我們看個(gè)例子來理解這種機(jī)制:

>>> a = 'Real Python'
>>> b = ['Real', 'Python']
>>> len(a)
11
>>> a.__len__()
11
>>> b[0]
'Real'
>>> b.__getitem__(0)
'Real'

如你所見,當(dāng)你分別使用函數(shù)或者關(guān)聯(lián)的特定方法時(shí)帽哑,你獲得了同樣的結(jié)果谜酒。實(shí)際上祝拯,如果你使用內(nèi)置函數(shù) dir() 列出一個(gè)字符串對象的所有方法和屬性甚带,你也可以在里面找到這些特定方法:

>>> dir(a)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 ...,
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 ...,
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

如果內(nèi)置函數(shù)或運(yùn)算符的行為沒有在類中特定方法中定義,你會(huì)得到一個(gè)類型錯(cuò)誤。
那么枝哄,如何在你的類中使用特定方法呢蓖租?

重載內(nèi)置函數(shù)

數(shù)據(jù)模型中定義的大多數(shù)特定方法都可以用來改變 len, abs, hash, divmod 等內(nèi)置函數(shù)的行為。你只需要在你的類中定義好關(guān)聯(lián)的特定方法就好了。下面舉幾個(gè)栗子:

用 len() 函數(shù)獲取你對象的長度

要更改 len() 的行為鲫售,你需要在你的類中定義 len() 這個(gè)特定方法。每次你傳入類的實(shí)例對象給 len() 時(shí)雏蛮,它都會(huì)通過你定義的 len() 來返回結(jié)果犀概。下面,我們來實(shí)現(xiàn)前面 order 類的 len() 函數(shù)的行為:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __len__(self):
...         return len(self.cart)
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)
3

如你所見曾沈,你現(xiàn)在可以直接使用 len() 來獲得購物車列表長度卧土。相比 order.get_cart_len() 調(diào)用方式生棍,使用 len() 更符合“隊(duì)列長度”這個(gè)直觀表述柔纵,你的代碼調(diào)用更 Pythonic系羞,更符合直觀習(xí)慣庐杨。如果你沒有定義 len() 這個(gè)方法罩引,當(dāng)你調(diào)用 len() 時(shí)就會(huì)返回一個(gè)類型錯(cuò)誤:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)  # Calling len when no __len__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Order' has no len()

此外屉更,當(dāng)你重載 len() 時(shí)隐轩,你需要記住的是 Python 需要該函數(shù)返回的是一個(gè)整數(shù)值鹊杖,如果你的方法函數(shù)返回的是除整數(shù)外的其它值,也會(huì)報(bào)類型錯(cuò)誤(TypeError)筐钟。此做法很可能是為了與 len() 通常用于獲取序列的長度這種用途(序列的長度只能是整數(shù))保持一致:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __len__(self):
...         return float(len(self.cart))  # Return type changed to float
...
>>> order = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> len(order)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

讓你的對象提供 abs() 運(yùn)算

你可以通過定義類的 abs() 方法來控制內(nèi)置函數(shù) abs() 作用于實(shí)例對象時(shí)的行為妇菱。abs() 函數(shù)對返回值沒有約束,只是在你的類沒有定義關(guān)聯(lián)的特定方法時(shí)會(huì)得到類型錯(cuò)誤刃唤。
在表示二維空間向量的類中崖堤, abs() 函數(shù)可以被用來獲取向量的長度胯甩。下面演示如何做:

>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __abs__(self):
...         return (x * x + y * y) ** 0.5
...
>>> vector = Vector(3, 4)
>>> abs(vector)
5.0

這樣表述為“向量的絕對值”相對于 vector.get_mag() 這樣的調(diào)用會(huì)顯得更直觀怜森。

通過 str() 提供更加美觀的對象輸出格式

內(nèi)置函數(shù) str() 通常用于將類實(shí)例轉(zhuǎn)換為字符串對象培己,更準(zhǔn)確地說,為普通用戶提供更友好的字符串表示方式比肄,而不僅僅是面向程序員嘹害。通過在你的類定義中實(shí)現(xiàn) str() 特定方法你可以自定義你的對象使用 str() 輸出時(shí)的字符串輸出格式授账。此外,當(dāng)你使用 print() 輸出你的對象時(shí) Python 實(shí)際上調(diào)用的也是 str() 方法纱昧。
我們將在 Vector 類中實(shí)現(xiàn) Vector 對象的輸出格式為 xi+yj。負(fù)的 Y 方向的分量輸出格式使用迷你語言來處理:

>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __str__(self):
...         # By default, sign of +ve number is not displayed
...         # Using `+`, sign is always displayed
...         return f'{self.x_comp}i{self.y_comp:+}j'
...
>>> vector = Vector(3, 4)
>>> str(vector)
'3i+4j'
>>> print(vector)
3i+4j

需要注意的是 str() 必須返回一個(gè)字符串對象卦尊,如果我們返回值的類型為非字符串類型,將會(huì)報(bào)類型錯(cuò)誤。

使用 repr() 來顯示你的對象

repr() 內(nèi)置函數(shù)通常用來獲取對象的可解析字符串表示形式授瘦。如果一個(gè)對象是可解析的戏羽,這意味著使用 repr 再加上 eval() 此類函數(shù),Python 就可以通過字符串表述來重建對象朦前。要定義 repr() 函數(shù)的行為浇揩,你可以通過定義 repr() 方法來實(shí)現(xiàn)聂儒。

這也是 Python 在 REPL(交互式)會(huì)話中顯示一個(gè)對象所使用的方式 。如果 repr() 方法沒有定義,你在 REPL 會(huì)話中試圖輸出一個(gè)對象時(shí)柳恐,會(huì)得到類似 <main.Vector object at 0x...> 這樣的結(jié)果她倘。我們來看 Vector 類這個(gè)例子的實(shí)際運(yùn)行情況:

>>> class Vector:
...     def __init__(self, x_comp, y_comp):
...         self.x_comp = x_comp
...         self.y_comp = y_comp
...
...     def __repr__(self):
...         return f'Vector({self.x_comp}, {self.y_comp})'
...

>>> vector = Vector(3, 4)
>>> repr(vector)
'Vector(3, 4)'

>>> b = eval(repr(vector))
>>> type(b), b.x_comp, b.y_comp
(__main__.Vector, 3, 4)

>>> vector  # Looking at object; __repr__ used
'Vector(3, 4)'

注意:如果 str() 方法沒有定義帝牡,當(dāng)在對象上調(diào)用 str() 函數(shù),Python 會(huì)使用 repr() 方法來代替蒙揣,如果兩者都沒有定義靶溜,默認(rèn)輸出為 <main.Vector ...>。在交互環(huán)境中 repr() 是用來顯示對象的唯一方式懒震,類定義中缺少它罩息,只會(huì)輸出 <main.Vector ...>。
盡管个扰,這是官方推薦的兩者行為的區(qū)別瓷炮,但在很多流行的庫中實(shí)際上都忽略了這種行為差異,而交替使用它們递宅。
關(guān)于 repr() 和 str() 的問題推薦閱讀 Dan Bader 寫的這篇比較出名的文章:Python 字符串轉(zhuǎn)換 101:為什么每個(gè)類都需要定義一個(gè) “repr”

使用 bool() 提供布爾值判斷

內(nèi)置的函數(shù) bool() 可以用來提供真值檢測娘香,要定義它的行為,你可以通過定義 bool() (Python 2.x版是 nonzero())特定方法來實(shí)現(xiàn)办龄。
此處的定義將供所有需要判斷真值的上下文(比如 if 語句)中使用烘绽。比如,前面定義的 Order 類俐填,某個(gè)實(shí)例中可能需要判斷購物車長度是否為非零安接。用來檢測是否繼續(xù)處理訂單:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __bool__(self):
...         return len(self.cart) > 0
...
>>> order1 = Order(['banana', 'apple', 'mango'], 'Real Python')
>>> order2 = Order([], 'Python')

>>> bool(order1)
True
>>> bool(order2)
False

>>> for order in [order1, order2]:
...     if order:
...         print(f"{order.customer}'s order is processing...")
...     else:
...         print(f"Empty order for customer {order.customer}")

Real Python's order is processing...
Empty order for customer Python

注意:如果類的 bool() 特定方法沒有定義, len() 方法返回值將會(huì)用來做真值判斷英融,如果是一個(gè)非零值則為真盏檐,零值為假歇式。如果兩個(gè)方法都沒有被定義,此類的所有實(shí)例檢測都會(huì)被判斷為真值胡野。

還有更多用來重載內(nèi)置函數(shù)的特定方法材失,你可以在官方文檔中找到它們的用法,下面我們開始討論運(yùn)算符重載的問題给涕。

重載內(nèi)置運(yùn)算符

要改變一個(gè)運(yùn)算符的行為跟改變函數(shù)的行為一樣豺憔,很簡單。你只需在類中定義好對應(yīng)的特定方法够庙,運(yùn)算符就會(huì)按照你設(shè)定的方式運(yùn)行恭应。

跟上面的特定方法不同的是,這些方法定義中耘眨,除了接收自身(self)這個(gè)參數(shù)外昼榛,它還需要另一個(gè)參數(shù)
下面,我們看幾個(gè)例子剔难。

讓你的對象能夠使用 + 運(yùn)算符做加法運(yùn)算

與 + 運(yùn)算符對應(yīng)的特定方法是 add() 方法胆屿。添加一個(gè)自定義的 add() 方法將會(huì)改變該運(yùn)算符的行為。建議讓 add() 方法返回一個(gè)新的實(shí)例對象而不要修改調(diào)用的實(shí)例本身偶宫。在 Python 中非迹,這種行為非常常見:

>>> a = 'Real'
>>> a + 'Python'  # Gives new str instance
'RealPython'
>>> a  # Values unchanged
'Real'
>>> a = a + 'Python'  # Creates new instance and assigns a to it
>>> a
'RealPython'

你會(huì)發(fā)現(xiàn)上面例子中字符串對象進(jìn)行 + 運(yùn)算會(huì)返回一個(gè)新的字符串,原來的字符串本身并沒有被改變纯趋。要改變這種方式憎兽,我們需要顯式地將生成的新實(shí)例賦值給 a。

我們將在 Order 類中實(shí)現(xiàn)通過 + 運(yùn)算符來將新的項(xiàng)目添加到購物車中吵冒。我們遵循推薦的方法纯命,運(yùn)算后返回一個(gè)新的 Order 實(shí)例對象而不是直接更改現(xiàn)有實(shí)例對象的值:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __add__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.append(other)
...         return Order(new_cart, self.customer)
...
>>> order = Order(['banana', 'apple'], 'Real Python')

>>> (order + 'orange').cart  # New Order instance
['banana', 'apple', 'mango']
>>> order.cart  # Original instance unchanged
['banana', 'apple']

>>> order = order + 'mango'  # Changing the original instance
>>> order.cart
['banana', 'apple', 'mango']

同樣的,還有其他的 sub(), mul() 等等特定方法痹栖,它們分別對應(yīng) -亿汞,*,等等運(yùn)算符揪阿。它們也都是返回新的實(shí)例對象疗我。

一種快捷方式:+= 運(yùn)算符

+= 運(yùn)算符通常作為表達(dá)式 obj1 = obj1 + obj2 的一種快捷方式。對應(yīng)的特定方法是 iadd()南捂,該方法會(huì)直接修改自身的值碍粥,返回的結(jié)果可能是自身也可能不是自身。這一點(diǎn)跟 add() 方法有很大的區(qū)別黑毅,后者是生成新對象作為結(jié)果返回。

大致來說钦讳,+= 運(yùn)算符等價(jià)于:

>>> result = obj1 + obj2
>>> obj1 = result

上面枕面,result 是 iadd() 返回的值。第二步賦值是 Python 自動(dòng)處理的缚去,也就是說你無需顯式地用表達(dá)式 obj1 = obj1 + obj2 將結(jié)果賦值給 obj1 潮秘。
我們將在 Order 類中實(shí)現(xiàn)這個(gè)功能,這樣我們就可以使用 += 來添加新項(xiàng)目到購物車中:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...         return self
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order.cart
['banana', 'apple', 'mango']

如上所見易结,所有的更改是直接作用在對象自身上枕荞,并返回自身。如果我們讓它返回一些隨機(jī)值比如字符串搞动、整數(shù)怎樣躏精?

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...         return 'Hey, I am string!'
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order
'Hey, I am string!'

盡管,我們往購物車?yán)锾砑拥氖窍嚓P(guān)的項(xiàng)鹦肿,但購物車的值卻變成了 iadd() 返回的值矗烛。Python 在后臺隱式處理這個(gè)過程。如果你在方法實(shí)現(xiàn)中忘記處理返回內(nèi)容箩溃,可能會(huì)出現(xiàn)令人驚訝的行為:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __iadd__(self, other):
...         self.cart.append(other)
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order += 'mango'
>>> order  # No output
>>> type(order)
NoneType

Python 中所有的函數(shù)(方法)默認(rèn)都是返回 None瞭吃,因此,order 的值被設(shè)置為默認(rèn)值 None涣旨,交互界面不會(huì)有輸出顯示歪架。如果檢查 order 的類型,顯示為 NoneType 類型霹陡。因此和蚪,你需要確保在 iadd() 的實(shí)現(xiàn)中返回期望得到的結(jié)果而不是其他什么東東。

iadd() 類似穆律, isub(), imul(), idiv() 等特定方法相應(yīng)地定義了 -=, *=, /= 等運(yùn)算符的行為惠呼。

注意:當(dāng) iadd() 或者同系列的方法沒有在你的類中定義,而你又在你的對象上使用這些運(yùn)算符時(shí)峦耘。Python 會(huì)用 add() 系列方法來替代并返回結(jié)果剔蹋。通常來講,如果 add() 系列方法能夠返回預(yù)期正確的結(jié)果辅髓,不使用 iadd() 系列的方法是一種安全的方式泣崩。

Python 的文檔提供了這些方法的詳細(xì)說明。此外洛口,可以看看當(dāng)使用不可變類型涉及到的 +=及其他運(yùn)算符需要注意到的附加說明的代碼實(shí)例矫付。

使用 [] 運(yùn)算符來索引和分片你的對象

[] 運(yùn)算符被稱作索引運(yùn)算符,在 Python 各上下文中都有用到第焰,比如獲取序列某個(gè)索引的值买优,獲取字典某個(gè)鍵對應(yīng)的值,或者對序列的切片操作。你可以通過 getitem() 特定方法來控制該運(yùn)算符的行為杀赢。

我們設(shè)置一下 Order 類的定義烘跺,讓我們可以直接獲取購物車對象中的項(xiàng):

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __getitem__(self, key):
...         return self.cart[key]
...
>>> order = Order(['banana', 'apple'], 'Real Python')
>>> order[0]
'banana'
>>> order[-1]
'apple'

你可能會(huì)注意到上面的例子中, getitem() 方法的參數(shù)名并不是 index 而是 key脂崔。這是因?yàn)槁舜荆瑓?shù)主要接收三種類型的值:整數(shù)值,通常是一個(gè)索引或字典的鍵值砌左;字符串脖咐,字典的鍵值;切片對象汇歹,序列對象的切片屁擅。當(dāng)然,也可能會(huì)有其他的值類型秤朗,但這三種是最常見的形式煤蹭。
因?yàn)槲覀兊膬?nèi)部數(shù)據(jù)結(jié)構(gòu)是一個(gè)列表,我們可以使用 [] 運(yùn)算符來對列表進(jìn)行切片取视,這時(shí) key 參數(shù)會(huì)接收一個(gè)切片對象硝皂。這就是在類中定義 getitem() 方法的最大優(yōu)勢。只要你使用的數(shù)據(jù)結(jié)構(gòu)支持切片操作(列表作谭、元組稽物、字符串等等),你就可以定義你的對象直接對數(shù)據(jù)進(jìn)行切片:

>>> order[1:]
['apple']
>>> order[::-1]
['apple', 'banana']

注意:有一個(gè)類似的 setitem() 特定方法可定義類似 obj[x] = y 這種行為折欠。此方法除自身外還需要兩個(gè)參數(shù)贝或,一般稱為 key 和 value,用來更改指定 key 索引的值锐秦。

逆運(yùn)算符:讓你的類在數(shù)學(xué)計(jì)算上正確

在你定義了 add(), sub(), mul()咪奖,以及類似的方法后,類實(shí)例作為左側(cè)操作數(shù)時(shí)可以正確運(yùn)行酱床,但如果作為右側(cè)操作數(shù)則不會(huì)正常工作:

>>> class Mock:
...     def __init__(self, num):
...         self.num = num
...     def __add__(self, other):
...         return Mock(self.num + other)
...
>>> mock = Mock(5)
>>> mock = mock + 6
>>> mock.num
11

>>> mock = 6 + Mock(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Mock'

如果你的類表示的是一個(gè)數(shù)學(xué)實(shí)體羊赵,比如向量、坐標(biāo)或復(fù)數(shù)扇谣,運(yùn)算符應(yīng)該在這兩種方式下都能正確運(yùn)算昧捷,因?yàn)樗怯行У臄?shù)學(xué)運(yùn)算規(guī)則。此外罐寨,如果某個(gè)運(yùn)算符僅僅在操作數(shù)為左側(cè)時(shí)才工作靡挥,這在數(shù)學(xué)上違背了交換律規(guī)則。因此鸯绿,為了保證在數(shù)學(xué)上的正確跋破,Python 為你提供了反向計(jì)算的 radd(), rsub(), rmul()等特定方法簸淀。

這些方法處理類似 x + obj, x - obj, 以及 x * obj 形式的運(yùn)算,其中 x 不是一個(gè)類實(shí)例對象毒返。和 add() 及其他方法一樣啃擦,這些方法也應(yīng)該返回一個(gè)新的實(shí)例對象,而不是修改自身饿悬。
我們在 Order 類中定義 radd() 方法,這樣就可以將某些項(xiàng)操作數(shù)放在購物車對象前面進(jìn)行添加聚霜。這還可以用在購物車內(nèi)訂單是按照優(yōu)先次序排列的情況狡恬。:

>>> class Order:
...     def __init__(self, cart, customer):
...         self.cart = list(cart)
...         self.customer = customer
...
...     def __add__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.append(other)
...         return Order(new_cart, self.customer)
...
...     def __radd__(self, other):
...         new_cart = self.cart.copy()
...         new_cart.insert(0, other)
...         return Order(new_cart, self.customer)
...
>>> order = Order(['banana', 'apple'], 'Real Python')

>>> order = order + 'orange'
>>> order.cart
['banana', 'apple', 'orange']

>>> order = 'mango' + order
>>> order.cart
['mango', 'banana', 'apple', 'orange']

一個(gè)完整的例子

想要掌握以上所有的關(guān)鍵點(diǎn),最好自己實(shí)現(xiàn)一個(gè)包含以上所有操作的自定義類蝎宇。我們自己來造一個(gè)輪子弟劲,實(shí)現(xiàn)一個(gè)復(fù)數(shù)的自定義類 CustomComplex。這個(gè)類的實(shí)例將支持各種內(nèi)置函數(shù)和運(yùn)算符姥芥,行為表現(xiàn)上非常類似于 Python 自帶的復(fù)數(shù)類:

from math import hypot, atan, sin, cos

class CustomComplex:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

構(gòu)造函數(shù)只支持一種調(diào)用方式兔乞,即 CustomComplex(a, b)。它通過位置參數(shù)來表示復(fù)數(shù)的實(shí)部和虛部凉唐。我們在這個(gè)類中定義兩個(gè)方法 conjugate() 和 argz()庸追。它們分別提供復(fù)數(shù)共軛和復(fù)數(shù)的輻角:

def conjugate(self):
    return self.__class__(self.real, -self.imag)

def argz(self):
    return atan(self.imag / self.real)

注意: class 并不是特定方法,只是默認(rèn)的一個(gè)類屬性通常指向類本身台囱。這里我們跟調(diào)用構(gòu)造函數(shù)一樣來對它進(jìn)行調(diào)用淡溯,換句話來說其實(shí)調(diào)用的就是 CustomComplex(real, imag)。這樣調(diào)用是為了防止今后更改類名時(shí)要再次重構(gòu)代碼簿训。
下一步咱娶,我們配置 abs() 返回復(fù)數(shù)的模:

def __abs__(self):
    return hypot(self.real, self.imag)

我們遵循官方建議的 repr() 和 str() 兩者差異,用第一個(gè)來實(shí)現(xiàn)可解析的字符串輸出强品,用第二個(gè)來實(shí)現(xiàn)“更美觀”的輸出膘侮。 repr() 方法簡單地返回 CustomComplex(a, b) 字符串,這樣我們在調(diào)用 eval() 重建對象時(shí)很方便的榛。 str() 方法用來返回帶括號的復(fù)數(shù)輸出形式琼了,比例 (a+bj):

def __repr__(self):
    return f"{self.__class__.__name__}({self.real}, {self.imag})"

def __str__(self):
    return f"({self.real}{self.imag:+}j)"

數(shù)學(xué)上講,我們可以進(jìn)行兩個(gè)復(fù)數(shù)相加或者將一個(gè)實(shí)數(shù)和復(fù)數(shù)相加困曙。我們定義 + 運(yùn)算符來實(shí)現(xiàn)這個(gè)功能表伦。方法將會(huì)檢測運(yùn)算符右側(cè)的類型,如果是一個(gè)整數(shù)或者浮點(diǎn)數(shù)慷丽,它將只增加實(shí)部(因?yàn)槿我鈱?shí)數(shù)都可以看做是 a+0j)蹦哼,當(dāng)類型是復(fù)數(shù)時(shí),它會(huì)同時(shí)更改實(shí)部和虛部:

def __add__(self, other):
    if isinstance(other, float) or isinstance(other, int):
        real_part = self.real + other
        imag_part = self.imag

    if isinstance(other, CustomComplex):
        real_part = self.real + other.real
        imag_part = self.imag + other.imag

    return self.__class__(real_part, imag_part)

同樣要糊,我們定義 -* 運(yùn)算符的行為:

def __sub__(self, other):
    if isinstance(other, float) or isinstance(other, int):
        real_part = self.real - other
        imag_part = self.imag

    if isinstance(other, CustomComplex):
        real_part = self.real - other.real
        imag_part = self.imag - other.imag

    return self.__class__(real_part, imag_part)

def __mul__(self, other):
    if isinstance(other, int) or isinstance(other, float):
        real_part = self.real * other
        imag_part = self.imag * other

    if isinstance(other, CustomComplex):
        real_part = (self.real * other.real) - (self.imag * other.imag)
        imag_part = (self.real * other.imag) + (self.imag * other.real)

    return self.__class__(real_part, imag_part)

因?yàn)榧臃ê统朔梢越粨Q操作數(shù)纲熏,我們可以在反向運(yùn)算符 radd() 和 rmul() 方法中這樣調(diào)用 add() 和 mul() 。此外,減法運(yùn)算的操作數(shù)是不可以交換的局劲,所以需要 rsub() 方法的行為:

def __radd__(self, other):
    return self.__add__(other)

def __rmul__(self, other):
    return self.__mul__(other)

def __rsub__(self, other):
    # x - y != y - x
    if isinstance(other, float) or isinstance(other, int):
        real_part = other - self.real
        imag_part = -self.imag

    return self.__class__(real_part, imag_part)

注意:你也許發(fā)現(xiàn)我們并沒有增添一個(gè)構(gòu)造函數(shù)來處理 CustomComplex 實(shí)例勺拣。因?yàn)檫@種情形下,兩個(gè)操作數(shù)都是類的實(shí)例鱼填, rsub() 方法并不負(fù)責(zé)處理實(shí)際的運(yùn)算药有,僅僅是調(diào)用 sub() 方法來處理。這是一個(gè)微妙但是很重要的細(xì)節(jié)苹丸。
現(xiàn)在我們來看看另外兩個(gè)運(yùn)算符:== 和 != 愤惰。這兩個(gè)分別對應(yīng)的特定方法是 eq() 和 ne()。如果兩個(gè)復(fù)數(shù)的實(shí)部和虛部都相同則兩者是相等的赘理。只要兩個(gè)部分任意一個(gè)不相等兩者就不相等:

def __eq__(self, other):
    # Note: generally, floats should not be compared directly
    # due to floating-point precision
    return (self.real == other.real) and (self.imag == other.imag)

def __ne__(self, other):
    return (self.real != other.real) or (self.imag != other.imag)

注意:浮點(diǎn)指南這篇文章討論了浮點(diǎn)數(shù)比較和浮點(diǎn)精度的問題宦言,它涉及到一些浮點(diǎn)數(shù)直接比較的一些注意事項(xiàng),這與我們在這里要處理的情況有點(diǎn)類似商模。
同樣奠旺,我們也可以通過簡單的公式來提供復(fù)數(shù)的冪運(yùn)算。我們通過定義 pow() 特定方法來設(shè)置內(nèi)置函數(shù) pow() 和 ** 運(yùn)算符的行為:

def __pow__(self, other):
    r_raised = abs(self) ** other
    argz_multiplied = self.argz() * other

    real_part = round(r_raised * cos(argz_multiplied))
    imag_part = round(r_raised * sin(argz_multiplied))

    return self.__class__(real_part, imag_part)

注意:認(rèn)真看看方法的定義施流。我們調(diào)用 abs() 來獲取復(fù)數(shù)的模响疚。所以,我們一旦為特定功能函數(shù)或運(yùn)算符定義好了特定方法嫂沉,它就可以被用于此類的其他方法中稽寒。

我們創(chuàng)建這個(gè)類的兩個(gè)實(shí)例,一個(gè)擁有正的虛部趟章,一個(gè)擁有負(fù)的虛部:

>>> a = CustomComplex(1, 2)
>>> b = CustomComplex(3, -4)

字符串表示:

>>> a
CustomComplex(1, 2)
>>> b
CustomComplex(3, -4)
>>> print(a)
(1+2j)
>>> print(b)
(3-4j)

使用 eval() 和 repr()重建對象

>>> b_copy = eval(repr(b))
>>> type(b_copy), b_copy.real, b_copy.imag
(__main__.CustomComplex, 3, -4)

加減乘法:

>>> a + b
CustomComplex(4, -2)
>>> a - b
CustomComplex(-2, 6)
>>> a + 5
CustomComplex(6, 2)
>>> 3 - a
CustomComplex(2, -2)
>>> a * 6
CustomComplex(6, 12)
>>> a * (-6)
CustomComplex(-6, -12)

相等和不等檢測:

>>> a == CustomComplex(1, 2)
True
>>> a ==  b
False
>>> a != b
True
>>> a != CustomComplex(1, 2)
False

最后杏糙,將復(fù)數(shù)加到某個(gè)冪上:

```python
>>> a ** 2
CustomComplex(-3, 4)
>>> b ** 5
CustomComplex(-237, 3116)

正如你所見到的,我們自定義類的對象外觀及行為上類似于內(nèi)置的對象而且很 Pythonic蚓土。

回顧總結(jié)

本教程中宏侍,你學(xué)習(xí)了 Python 數(shù)據(jù)模型,以及如何通過數(shù)據(jù)模型來構(gòu)建 Pythonic 的類蜀漆。學(xué)習(xí)了改變 len(), abs(), str(), bool() 等內(nèi)置函數(shù)的行為谅河,以及改變 +, -, *, **, 等內(nèi)置運(yùn)算符的行為。
如果想要進(jìn)一步地了解數(shù)據(jù)模型确丢、函數(shù)和運(yùn)算符重載绷耍,請參考以下資源:

本示例的完整代碼:

from math import hypot, atan, sin, cos

class CustomComplex():
    """
    A class to represent a complex number, a+bj.
    Attributes:
        real - int, representing the real part
        imag - int, representing the imaginary part

    Implements the following:

    * Addition with a complex number or a real number using `+`
    * Multiplication with a complex number or a real number using `*`
    * Subtraction of a complex number or a real number using `-`
    * Calculation of absolute value using `abs`
    * Raise complex number to a power using `**`
    * Nice string representation using `__repr__`
    * Nice user-end viewing using `__str__`

    Notes:
        * The constructor has been intentionally kept simple
        * It is configured to support one kind of call:
            CustomComplex(a, b)
        * Error handling was avoided to keep things simple
    """

    def __init__(self, real, imag):
        """
        Initializes a complex number, setting real and imag part
        Arguments:
            real: Number, real part of the complex number
            imag: Number, imaginary part of the complex number
        """
        self.real = real
        self.imag = imag

    def conjugate(self):
        """
        Returns the complex conjugate of a complex number
        Return:
            CustomComplex instance
        """
        return CustomComplex(self.real, -self.imag)

    def argz(self):
        """
        Returns the argument of a complex number
        The argument is given by:
            atan(imag_part/real_part)
        Return:
            float
        """
        return atan(self.imag / self.real)

    def __abs__(self):
        """
        Returns the modulus of a complex number
        Return:
            float
        """
        return hypot(self.real, self.imag)

    def __repr__(self):
        """
        Returns str representation of an instance of the 
        class. Can be used with eval() to get another 
        instance of the class
        Return:
            str
        """
        return f"CustomComplex({self.real}, {self.imag})"


    def __str__(self):
        """
        Returns user-friendly str representation of an instance 
        of the class
        Return:
            str
        """
        return f"({self.real}{self.imag:+}j)"

    def __add__(self, other):
        """
        Returns the addition of a complex number with
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real + other
            imag_part = self.imag

        if isinstance(other, CustomComplex):
            real_part = self.real + other.real
            imag_part = self.imag + other.imag

        return CustomComplex(real_part, imag_part)

    def __sub__(self, other):
        """
        Returns the subtration from a complex number of
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = self.real - other
            imag_part = self.imag

        if isinstance(other, CustomComplex):
            real_part = self.real - other.real
            imag_part = self.imag - other.imag

        return CustomComplex(real_part, imag_part)

    def __mul__(self, other):
        """
        Returns the multiplication of a complex number with
        int, float or another complex number
        Return:
            CustomComplex instance
        """
        if isinstance(other, int) or isinstance(other, float):
            real_part = self.real * other
            imag_part = self.imag * other

        if isinstance(other, CustomComplex):
            real_part = (self.real * other.real) - (self.imag * other.imag)
            imag_part = (self.real * other.imag) + (self.imag * other.real)

        return CustomComplex(real_part, imag_part)

    def __radd__(self, other):
        """
        Same as __add__; allows 1 + CustomComplex('x+yj')
        x + y == y + x
        """
        pass

    def __rmul__(self, other):
        """
        Same as __mul__; allows 2 * CustomComplex('x+yj')
        x * y == y * x
        """
        pass

    def __rsub__(self, other):
        """
        Returns the subtraction of a complex number from
        int or float
        x - y != y - x
        Subtration of another complex number is not handled by __rsub__
        Instead, __sub__ handles it since both sides are instances of
        this class
        Return:
            CustomComplex instance
        """
        if isinstance(other, float) or isinstance(other, int):
            real_part = other - self.real
            imag_part = -self.imag

        return CustomComplex(real_part, imag_part)

    def __eq__(self, other):
        """
        Checks equality of two complex numbers
        Two complex numbers are equal when:
            * Their real parts are equal AND
            * Their imaginary parts are equal
        Return:
            bool
        """
        # note: comparing floats directly is not a good idea in general
        # due to floating-point precision
        return (self.real == other.real) and (self.imag == other.imag)

    def __ne__(self, other):
        """
        Checks inequality of two complex numbers
        Two complex numbers are unequal when:
            * Their real parts are unequal OR
            * Their imaginary parts are unequal
        Return:
            bool
        """
        return (self.real != other.real) or (self.imag != other.imag)

    def __pow__(self, other):
        """
        Raises a complex number to a power
        Formula:
            z**n = (r**n)*[cos(n*agrz) + sin(n*argz)j], where
            z = complex number
            n = power
            r = absolute value of z
            argz = argument of z
        Return:
            CustomComplex instance
        """
        r_raised = abs(self) ** other
        argz_multiplied = self.argz() * other

        real_part = round(r_raised * cos(argz_multiplied))
        imag_part = round(r_raised * sin(argz_multiplied))

        return CustomComplex(real_part, imag_part)

本作品采用:知識共享 署名-非商業(yè)性使用-禁止演繹 4.0 國際許可協(xié)議進(jìn)行許可褂始。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市描函,隨后出現(xiàn)的幾起案子崎苗,更是在濱河造成了極大的恐慌狐粱,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胆数,死亡現(xiàn)場離奇詭異肌蜻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)必尼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門蒋搜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人判莉,你說我怎么就攤上這事齿诞。” “怎么了骂租?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斑司。 經(jīng)常有香客問我渗饮,道長,這世上最難降的妖魔是什么宿刮? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任互站,我火速辦了婚禮,結(jié)果婚禮上僵缺,老公的妹妹穿的比我還像新娘胡桃。我一直安慰自己,他們只是感情好磕潮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布翠胰。 她就那樣靜靜地躺著,像睡著了一般自脯。 火紅的嫁衣襯著肌膚如雪之景。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天膏潮,我揣著相機(jī)與錄音锻狗,去河邊找鬼。 笑死焕参,一個(gè)胖子當(dāng)著我的面吹牛轻纪,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播叠纷,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼刻帚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了讲岁?” 一聲冷哼從身側(cè)響起我擂,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤衬以,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后校摩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體看峻,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年衙吩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了互妓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坤塞,死狀恐怖冯勉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摹芙,我是刑警寧澤灼狰,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站浮禾,受9級特大地震影響交胚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜盈电,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一蝴簇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧匆帚,春花似錦熬词、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嚎幸,卻和暖如春摩幔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鞭铆。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工或衡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人车遂。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓封断,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舶担。 傳聞我的和親對象是個(gè)殘疾皇子坡疼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 〇、前言 本文共108張圖衣陶,流量黨請慎重柄瑰! 歷時(shí)1個(gè)半月闸氮,我把自己學(xué)習(xí)Python基礎(chǔ)知識的框架詳細(xì)梳理了一遍。 ...
    Raxxie閱讀 18,918評論 17 410
  • 一教沾、Python簡介和環(huán)境搭建以及pip的安裝 4課時(shí)實(shí)驗(yàn)課主要內(nèi)容 【Python簡介】: Python 是一個(gè)...
    _小老虎_閱讀 5,720評論 0 10
  • 根據(jù)公司領(lǐng)導(dǎo)的安排授翻,今天或悲,我給公司新入職不久的同事做了一場培訓(xùn),主題是“深入研究堪唐,巧妙應(yīng)答”巡语,目的是希望通過今天的...
    婧心婧力閱讀 594評論 2 9
  • A tiger. A running tiger. The tiger ran to Xiamen. The ti...
    悠悠半生緣閱讀 362評論 2 2
  • 讀書時(shí)候閱讀 170評論 0 0