原文: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)算符重載绷耍,請參考以下資源:
- Python 文檔,數(shù)據(jù)模型的第 3.3 節(jié)鲜侥,特定方法名
- 流暢的 Python(Fluent Python by Luciano Ramalho)
- Python 技巧(Python Tricks)
本示例的完整代碼:
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)行許可褂始。