自定義 Python 類中的運算符和函數重載(下)

自定义 Python 类中的运算符和函数重载(下)

重載內置運算符

改變運算符的行為與改變函數的行為一樣簡單。在類中定義其相應的特殊方法,運算符就會根據這些方法中定義的行為進行工作。

這些不同於上述特殊方法的意義是,除了self外他們需要接受另一參數,一般用other來指代。讓我們看看幾個例子。

使對象能夠使用 + 進行加操作

與運算符 + 對應的特殊方法是__add__方法。添加自定義__add__會改變運算符的行為。建議__add__返回類的新實例,而不是修改調用實例本身。在 Python 中這種行為很常見:

自定义 Python 类中的运算符和函数重载(下)

上面可以看到,對str對象使用運算符 + 實際上返回一個新str實例,從而保持調用實例 a 的值不被修改。要更改它, 我們需要顯式地將新實例賦給a。

讓我們實現在Order類中使用運算符向我們的購物車追加新項目的能力。我們將遵循建議的做法,並使運算符返回一個新的Order實例,它有我們所需的更改,而不是直接對我們的實例進行更改:

自定义 Python 类中的运算符和函数重载(下)

同樣, 還有__sub__,__mul__等其他重定義-,*的特殊方法。這些方法也應返回類的新實例。

縮寫:+ = 運算符

運算符+=是表達式obj1 = obj1 + obj2的縮寫。與之對應的特殊方法是__iadd__。__iadd__方法應直接對self參數進行更改,並返回結果 (可能也可能不是self)。此行為與後者創建新對象並返回的方式__add__完全不同,正如你在上面所看到的那樣。

大致而言, 對兩個對象使用+=都等同於:

自定义 Python 类中的运算符和函数重载(下)

這裡,result是__iadd__返回的值。第二次分配由 Python 自動處理,這意味著你不需要顯式分配obj1到結果,就像在obj1 = obj1 + obj2中的情況一樣。

讓我們在Order類中實現,以便新的項目可以追加到購物車使用:

自定义 Python 类中的运算符和函数重载(下)

可以看到, 任何更改都是直接對self進行的,然後返回。返回一些隨機值時會發生什麼情況,如字符串或整數?

自定义 Python 类中的运算符和函数重载(下)

儘管相關項目被追加到購物車中,但order的值更改為了__iadd__所返回的值。Python 隱式地處理了分配的任務。如果忘記在實現中返回某些內容,這可能會導致令人驚訝的結果:

自定义 Python 类中的运算符和函数重载(下)

由於所有 Python 函數 (或方法) 都是隱式返回None的,因此order被重新分配了None,REPL 會話在order檢查時不會顯示任何輸出。看order的類型,現在是NoneType。因此,請始終確保你在__iadd__實現中返回一些內容,並且它是運算的結果,而不是其他任何內容。

類似於__iadd__,你對定義-=,*=,/=有__isub__,__imul__,__idiv__等其他特殊的方法。

注:當你的類定義中缺失__iadd__等函數,但你仍對對象使用它們的運算符,Python 使用__add__等函數使用其運算符來獲取操作的結果並將其分配給調用實例。一般而言,只要在你的類中__add__等函數正常工作 (返回某種操作的結果),不實現__iadd__等函數就是安全的。

Python文檔對這些方法有很好的解釋。另外,請看一下這個示例,它顯示了使用+=等不可變類型時所涉及的警告和其他操作。

使用 對對象進行索引和切片

運算符稱為索引運算符,用於 Python 中的各種情境文中,例如在序列中的索引處獲取值、獲取與字典中的鍵值關聯的值或通過切片獲取序列的一部分。可以使用特殊方法__getitem__更改其行為。

讓我們配置我們的Order類,以便我們可以直接使用該對象並從購物車中獲取項目:

自定义 Python 类中的运算符和函数重载(下)

你會注意到__getitem__的參數名稱不是index而是key。這是因為該參數可以主要為三種形式:一個整數值,在這種情況下,它是一個索引或字典鍵值;一個字符串值,在這種情況下,它是一個字典鍵值;一個切片對象,在這種情況下,它將切片該類使用的序列。雖然還有其他可能性,但這些都是最常見的。

由於我們的內部數據結構是一個列表,我們可以使用運算符切片列表,就像在這種情況下,參數key將是切片對象。這是在你的類中定義__getitem__的最大優點之一。只要你使用支持切片的數據結構 (列表、元組、字符串等),就可以將對象配置為直接切片結構:

自定义 Python 类中的运算符和函数重载(下)

注:有一個類似的特殊方法__setitem__,用於定義obj[x] = y的行為。此方法除了self外接受兩個參數 (通常稱為key和value) ,還可用於將key對應值更改為value。

反向運算符: 使類在數學上正確

在定義__add__,__sub__,__mul__等類似的特殊方法時, 當類實例是左側操作數時,可以使用運算符,如果類實例是右側操作數,則運算符將無法工作:

自定义 Python 类中的运算符和函数重载(下)

如果你的類表示像向量、座標或複數等數學實體,則應用運算符應在兩種情況下都正常工作,因為它是有效的數學運算。

此外,如果運算符只在實例為左操作數時起作用,則在許多情況下,我們違反了交換律的基本原理。因此,為了幫助你使類在數學上正確,Python 提供了反向特殊方法,例如__radd__,__rsub__,__rmul__等等。

這些句柄調用 (如x + obj,x - obj和x * obj),其中x不是相關類的實例。就像__add__等函數一樣,這些反向特殊方法應返回一個修改後的新的類實例,而不是修改調用實例本身。

讓我們在Order類中配置__radd__,使其實現在購物車的前面追加一些東西的功能。當購物車按訂單的優先級組織時,可以使用此方法:

自定义 Python 类中的运算符和函数重载(下)

一個完整的示例

要歸納所有的這些點,最好看看一個實現這些運算符的示例類。

讓我們從實現我們自己的類CustomComplex來表示複數開始。我們的類對象將支持各種內置函數和運算符,使它們的行為與內置的複數類非常相似:

自定义 Python 类中的运算符和函数重载(下)

構造函數只處理一種調用CustomComplex(a, b)。它採用位置參數,表示複數的實部和虛部。

讓我們在類內定義兩種函數conjugate和argz,並分別給出複數共軛和複數的參數:

自定义 Python 类中的运算符和函数重载(下)

注:__class__不是特殊方法,而是默認情況下存在的類屬性。它有一個對類的引用。通過在這裡使用,我們得到了它,然後以通常的方式調用構造函數。換言之,這等同於CustomComplex(real, imag)。這樣做是為了避免如果某天類的名稱發生更改所要導致的重構代碼。

接下來我們配置abs來返回複數的模量:

自定义 Python 类中的运算符和函数重载(下)

我們將按照建議的__repr__和__str__的區別,為解析字符串表示形式使用前者,為了更"漂亮" 的表示後者。

__repr__方法將簡單地返回一個CustomComplex(a, b)字符串,以便我們可以調用eval重新創建對象,而__str__方法將返回括號中的複數,如(a+bj):

自定义 Python 类中的运算符和函数重载(下)

在數學上, 可以添加任意兩個複數或將實數添加到複數中。讓我們用這樣的方式配置運算符+,這樣它就可以在這兩種情況下工作。

該方法將檢查右側運算符的類型。如果它是int或float,它將只增加實部 (因為任何實數a等價於a+0j),而在是另一個複數的情況下,它的兩個部分都會更改:

自定义 Python 类中的运算符和函数重载(下)

同樣,我們定義-和*的行為:

自定义 Python 类中的运算符和函数重载(下)

由於加法和乘法都是有交換律的,我們可以通過在__radd__和__rmul__中分別調用__add__和__mul__來定義它們的反向算子。另一方面,由於減法是不可交換的,__rsub__的行為需要被定義:

自定义 Python 类中的运算符和函数重载(下)

注:你可能已經注意到, 我們沒有添加構造來處理CustomComplex實例。這是因為,在這種情況下,兩個操作數都是我們類的實例,__rsub__不負責處理操作。相反,__sub__將被調用。這是一個微妙但重要的細節。

現在,我們來看看這兩個運算符,==和!=。用於它們的特殊方法分別是__eq__和__ne__。如果兩個複數的實部和虛部分別相等,則它們是相等的。若其中任一不等,則兩個複數不等:

自定义 Python 类中的运算符和函数重载(下)

注:浮點數參考(http://floating-point-gui.de/errors/comparison/)是一篇討論瞭如何比較浮點精度和浮點數的文章。它強調了直接比較浮點的注意事項,這正是我們在這裡做的事情。

還可以使用簡單的公式得到複數的冪。我們使用特殊方法__pow__為內置的pow和運算符**配置行為:

自定义 Python 类中的运算符和函数重载(下)

注:仔細查看方法的定義。我們調用abs是為了得到複數的模量。因此,一旦定義了類中特定函數或運算符的特殊方法,就可以在同一類的其他方法中使用它。

讓我們創建這個類的兩個實例,一個具有正虛部,一個具有負虛部:

自定义 Python 类中的运算符和函数重载(下)

字符串表示形式:

自定义 Python 类中的运算符和函数重载(下)

使用帶repr的eval重新創建對象:

自定义 Python 类中的运算符和函数重载(下)

加法、減法和乘法:

自定义 Python 类中的运算符和函数重载(下)

相等不等檢查:

自定义 Python 类中的运算符和函数重载(下)

最後得到複數的冪:

自定义 Python 类中的运算符和函数重载(下)

正如你所看到的, 我們的自定義類的對象的行為和外觀類似於內置類,並且非常 Pythonic。此類的完整示例代碼如下:

自定义 Python 类中的运算符和函数重载(下)自定义 Python 类中的运算符和函数重载(下)
自定义 Python 类中的运算符和函数重载(下)自定义 Python 类中的运算符和函数重载(下)
自定义 Python 类中的运算符和函数重载(下)自定义 Python 类中的运算符和函数重载(下)

回顧和資源

在本教程中,你瞭解了 Python 數據模型以及數據模型如何用於生成 Pythonic 的類。你瞭解瞭如何更改內置函數 (如len、abs、str、bool等等) 的行為。你還了解了如何更改內置運算符的行為(如+、-、*、**等等)。

讀完本文後,你可以很好地創建類,利用 Python 的最佳慣用功能,使你的對象 Pythonic!

有關數據模型以及函數和運算符重載的更多信息,請查看以下資源:

  • 3.3 節,Python 文檔中數據模型部分的特殊方法名稱(https://docs.python.org/3/reference/datamodel.html#special-method-names )

  • Ramalho的《流暢 Python 》(https://www.amazon.cn/dp/1491946008/?tag=n )

  • Python 技巧(https://realpython.com/products/python-tricks-book/ )

英文原文:https://realpython.com/operator-function-overloading/
譯者:β


分享到:


相關文章: