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

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

原文:realpython.com/operator

如果你在字元串(str)對象上進行過 + 或 * 運算,你一定會注意到它跟整數或浮點數對象的行為差異:

>>> # 兩個數字相加>>> 1 + 23>>> # Concatenates the two strings>>> Real + PythonRealPython>>> # Gives the product>>> 3 * 26>>> # Repeats the string>>> Python * 3PythonPythonPython

你可能很好奇,為什麼同一個內置運算符或函數,作用在不同對象上面會表現出不同的行為。這種現象被稱為運算符重載或者函數重載。本文將幫助你理解這個機制,今後,你可以將它運用到你的自定義類中,讓你的編碼更 Pythonic 。

本文中你將學到:

  • Python 處理運算符和內置函數的API
  • len() 以及其它內置函數背後的「秘密」
  • 如何讓你的類可以使用運算符進行運算
  • 如何讓你的類與內置函數的操作保持兼容及行為一致

最後,我們將提供一個具體類的實例。該對象的行為與內置函數及運算符的行為保持一致。

Python 數據模型

假設,你有一個用來表示在線購物車的類,包含一個購物車(列表)和一名顧客(字元串或者其它表示顧客類的實例)。

這種情形下,很自然地需要獲取購物車的列表長度。Python 新手可能會考慮在他的類中實現一個叫 get_cart_len() 的方法來處理這個需求。實際上,你只需要配置一下,當我們傳入購物車實例對象時,使用內置函數 len() 就可以返回購物車的長度。

另一個場景中,我們可能需要添加某些商品到購物車。某些新手同樣會想要實現一個叫 append_to_cart() 的方法來處理獲取一個項,並將它添加到購物車列表中。其實你只需配置一下 + 運算符就可以實現將項目添加到購物車列表的操作。

Python 使用特定的方法來處理這些過程。這些特殊的方法都有一個特定的命名約定,以雙下劃線開始,後面跟命名標識符,最後以雙下劃線結束。

本質上講,每一種內置的函數或運算符都對應著對象的特定方法。比如,__len__() 方法對應內置 len() 函數,而 __add__() 方法對應 + 運算符。

默認情況下,絕大多數內置函數和運算符不會在你的類中工作。你需要在類定義中自己實現對應的特定方法,實例對象的行為才會和內置函數和運算符行為保持一致。當你完成這個過程,內置函數或運算符的操作才會如預期一樣工作

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

我們看看這意味著什麼。

軼事:由於這些方法的特殊命名方式,它們又被稱作 dunder 方法,是雙下劃線方法的簡稱。有時候它們也被稱作特殊方法或魔術方法。我們更喜歡 dunder 方法這個叫法。

len() 和 [] 的內部運行機制

每一個 Python 類都為內置函數或運算符定義了自己的行為方式。當你將某個實例對象傳入內置函數或使用運算符時,實際上等同於調用帶相應參數的特定方法。

如果有一個內置函數,func(),它關聯的特定方法是 __func__(),Python 解釋器會將其解釋為類似於 obj.__func__()的函數調用,obj 就是實例對象。如果是運算符操作,比如 opr ,關聯的特定方法是 __opr__(),Python 將 obj1 <opr> obj2 解釋為類似於 obj1.__opr__(obj2) 的形式。

所以,當你在實例對象上調用 len() 時,Python 將它處理為 obj.__len__() 調用。當你在可迭代對象上使用 [] 運算符來獲取指定索引位置上的值時,Python 將它處理為 itr.__getitem__(index),itr 表示可迭代對象,index 表示你要索引的位置。

因此,在你定義自己的類時,你可以重寫關聯的函數或運算符的行為。因為,Python 在後台調用的是你定義的方法。我們看個例子來理解這種機制:

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

如你所見,當你分別使用函數或者關聯的特定方法時,你獲得了同樣的結果。實際上,如果你使用內置函數 dir() 列出一個字元串對象的所有方法和屬性,你也可以在裡面找到這些特定方法:

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

如果內置函數或運算符的行為沒有在類中特定方法中定義,你會得到一個類型錯誤。那麼,如何在自己的類中使用特定方法呢?

重載內置函數

數據模型中定義的大多數特定方法都可以用來改變 len, abs, hash, divmod 等內置函數的行為。你只需要在你的類中定義好關聯的特定方法就好了。下面舉幾個栗子:

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

要更改 len() 的行為,你需要在你的類中定義 __len__() 這個特定方法。每次你傳入類的實例對象給 len() 時,它都會通過你定義的 __len__() 來返回結果。下面,我們來實現前面 order 類的 len() 函數的行為:

>>> 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

你現在可以直接使用 len() 來獲得購物車列表長度。相比 order.get_cart_len() 調用方式,使用 len() 更符合「隊列長度」這個直觀表述,你的代碼調用更 Pythonic,更符合直觀習慣。如果你沒有定義 __len__() 這個方法,當你調用 len() 時就會返回一個類型錯誤:

>>> 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()

此外,當你重載 len() 時,你需要記住的是 Python 需要該函數返回的是一個整數值,如果你的方法函數返回的是除整數外的其它值,也會報類型錯誤(TypeError)。此做法很可能是為了與 len() 通常用於獲取序列的長度這種用途(序列的長度只能是整數)保持一致:

>>> 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() 運算

我們可以通過定義類的 __abs__() 方法來控制內置函數 abs() 作用於實例對象時的行為。abs() 函數對返回值沒有約束,只是在你的類沒有定義關聯的特定方法時會得到類型錯誤。在表示二維空間向量的類中, abs() 函數可以被用來獲取向量的長度。下面演示如何做:

>>> 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() 這樣的調用會顯得更直觀。

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

內置函數 str() 通常用於將類實例轉換為字元串對象,更準確地說,為普通用戶提供更友好的字元串表示方式,而不僅僅是面向程序員。通過在你的類定義中實現 __str__() 特定方法你可以自定義你的對象使用 str() 輸出時的字元串輸出格式。此外,當你使用 print() 輸出你的對象時 Python 實際上調用的也是 __str__() 方法。

我們將在 Vector 類中實現 Vector 對象的輸出格式為 xi+yj。負的 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__() 必須返回一個字元串對象,如果我們返回值的類型為非字元串類型,將會報類型錯誤。

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

repr() 內置函數通常用來獲取對象的可解析字元串表示形式。如果一個對象是可解析的,這意味著使用 repr 再加上 eval() 此類函數,Python 就可以通過字元串表述來重建對象。要定義 repr() 函數的行為,你可以通過定義 __repr__() 方法來實現。

這也是 Python 在 REPL(互動式)會話中顯示一個對象所使用的方式 。如果 __repr__() 方法沒有定義,你在 REPL 會話中試圖輸出一個對象時,會得到類似 <main.Vector object at 0x...> 這樣的結果。我們來看 Vector 類這個例子的實際運行情況:

>>> class Vector:... def __init__(self, x_comp, y_comp):... self.x_comp = x_comp... self.y_comp = y_comp...... def __repr__(self):... return fVector({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__ usedVector(3, 4)

注意:如果 __str__() 方法沒有定義,當在對象上調用 str() 函數,Python 會使用 __repr__() 方法來代替,如果兩者都沒有定義,默認輸出為 <main.Vector ...>。在交互環境中 __repr__() 是用來顯示對象的唯一方式,類定義中缺少它,只會輸出 <main.Vector ...>。

儘管,這是官方推薦的兩者行為的區別,但在很多流行的庫中實際上都忽略了這種行為差異,而交替使用它們。

關於 __repr__()__str__() 的問題推薦閱讀 Dan Bader 寫的這篇比較出名的文章:Python 字元串轉換 101:為什麼每個類都需要定義一個 「repr」

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

內置的函數 bool() 可以用來提供真值檢測,要定義它的行為,你可以通過定義 __bool__() (Python 2.x版是 __nonzero__())特定方法來實現。

此處的定義將供所有需要判斷真值的上下文(比如 if 語句)中使用。比如,前面定義的 Order 類,某個實例中可能需要判斷購物車長度是否為非零。用來檢測是否繼續處理訂單:

>>> 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 Pythons order is processing...Empty order for customer Python

注意:如果類的 __bool__() 特定方法沒有定義, __len__() 方法返回值會被用來做真值判斷,如果是一個非零值則為真,零值為假。如果兩個方法都沒有被定義,此類的所有實例檢測都會被判斷為真值。

還有更多用來重載內置函數的特定方法,你可以在官方文檔中找到它們的用法,下面我們開始討論運算符重載的問題。

重載內置運算符

要改變一個運算符的行為跟改變函數的行為一樣,很簡單。你只需在類中定義好對應的特定方法,運算符就會按照你設定的方式運行。

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

讓對象能夠使用 + 運算符做加法運算

與 + 運算符對應的特定方法是 __add__() 方法。添加一個自定義的 __add__() 方法將會改變該運算符的行為。建議讓 __add__() 方法返回一個新的實例對象而不要修改調用的實例本身。在 Python 中,這種行為非常常見:

>>> a = Real>>> a + Python # Gives new str instanceRealPython>>> a # Values unchangedReal>>> a = a + Python # Creates new instance and assigns a to it>>> aRealPython

你會發現上面例子中字元串對象進行 + 運算會返回一個新的字元串,原來的字元串本身並沒有被改變。要改變這種方式,我們需要顯式地將生成的新實例賦值給 a。

我們將在 Order 類中實現通過 + 運算符來將新的項目添加到購物車中。我們遵循推薦的方法,運算後返回一個新的 Order 實例對象而不是直接更改現有實例對象的值:

>>> 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__() 等等特定方法,它們分別對應 -*,等等運算符。它們也都是返回新的實例對象。

一種快捷方式:+= 運算符

+= 運算符通常作為表達式 obj1 = obj1 + obj2 的一種快捷方式。對應的特定方法是 iadd(),該方法會直接修改自身的值,返回的結果可能是自身也可能不是自身。這一點跟 add() 方法有很大的區別,後者是生成新對象作為結果返回。

大致來說,+= 運算符等價於:

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

上面,result 是 __iadd__() 返回的值。第二步賦值是 Python 自動處理的,也就是說你無需顯式地用表達式 obj1 = obj1 + obj2 將結果賦值給 obj1 。

我們將在 Order 類中實現這個功能,這樣我們就可以使用 += 來添加新項目到購物車中:

>>> 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]

如上所見,所有的更改是直接作用在對象自身上,並返回自身。如果我們讓它返回一些隨機值比如字元串、整數怎樣?

>>> 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>>> orderHey, I am string!

儘管,我們往購物車裡添加的是相關的項,但購物車的值卻變成了 __iadd__() 返回的值。Python 在後台隱式處理這個過程。如果你在方法實現中忘記處理返回內容,可能會出現令人驚訝的行為:

>>> 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 中所有的函數(方法)默認都是返回 None,因此,order 的值被設置為默認值 None,交互界面不會有輸出顯示。如果檢查 order 的類型,顯示為 NoneType 類型。因此,你需要確保在 __iadd__() 的實現中返回期望得到的結果而不是其他什麼東東。

與 __iadd__() 類似, __isub__(), __imul__(), __idiv__() 等特定方法相應地定義了 -=, *=, /= 等運算符的行為。

注意:當 __iadd__() 或者同系列的方法沒有在你的類中定義,而你又在你的對象上使用這些運算符時。Python 會用 __add__() 系列方法來替代並返回結果。通常來講,如果 __add__() 系列方法能夠返回預期正確的結果,不使用 __iadd__() 系列的方法是一種安全的方式。

Python 的文檔提供了這些方法的詳細說明。此外,可以看看當使用不可變類型涉及到的 +=及其他運算符需要注意到的附加說明的代碼實例。

使用 [] 運算符來索引和分片你的對象

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

我們設置一下 Order 類的定義,讓我們可以直接獲取購物車對象中的項:

>>> 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

你可能會注意到上面的例子中, __getitem__() 方法的參數名並不是 index 而是 key。這是因為,參數主要接收三種類型的值:整數值,通常是一個索引或字典的鍵值;字元串,字典的鍵值;切片對象,序列對象的切片。當然,也可能會有其他的值類型,但這三種是最常見的形式。

因為我們的內部數據結構是一個列表,我們可以使用 [] 運算符來對列表進行切片,這時 key 參數會接收一個切片對象。這就是在類中定義 __getitem__() 方法的最大優勢。只要你使用的數據結構支持切片操作(列表、元組、字元串等等),你就可以定義你的對象直接對數據進行切片:

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

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

逆運算符:讓你的類在數學計算上正確

在你定義了 __add__(), __sub__(), __mul__(),以及類似的方法後,類實例作為左側操作數時可以正確運行,但如果作為右側操作數則不會正常工作:

>>> 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.num11>>> mock = 6 + Mock(5)Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: unsupported operand type(s) for +: int and Mock

如果你的類表示的是一個數學實體,比如向量、坐標或複數,運算符應該在這兩種方式下都能正確運算,因為它是有效的數學運算規則。此外,如果某個運算符僅僅在操作數為左側時才工作,這在數學上違背了交換律規則。因此,為了保證在數學上的正確,Python 為你提供了反向計算的 radd(), rsub(), rmul()等特定方法。

這些方法處理類似 x + obj, x - obj, 以及 x * obj 形式的運算,其中 x 不是一個類實例對象。和 __add__() 及其他方法一樣,這些方法也應該返回一個新的實例對象,而不是修改自身。

我們在 Order 類中定義 __radd__() 方法,這樣就可以將某些項操作數放在購物車對象前面進行添加。這還可以用在購物車內訂單是按照優先次序排列的情況。:

>>> 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]

一個完整的例子

要掌握以上關鍵點,最好自己實現一個包含以上所有操作的自定義類。下面我們自己來造一個輪子,實現一個複數的自定義類 CustomComplex。這個類的實例將支持各種內置函數和運算符,行為表現上將非常類似於 Python 自帶的複數類:

from math import hypot, atan, sin, cosclass CustomComplex: def __init__(self, real, imag): self.real = real self.imag = imag

構造函數只支持一種調用方式,即 CustomComplex(a, b)。它通過位置參數來表示複數的實部和虛部。我們在這個類中定義兩個方法 conjugate() 和 argz()。它們分別提供複數共軛和複數的輻角:

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

注意: class 並不是特定方法,只是默認的一個類屬性通常指向類本身。這裡我們跟調用構造函數一樣來對它進行調用,換句話來說其實調用的就是 CustomComplex(real, imag)。這樣調用是為了防止今後更改類名時要再次重構代碼。

下一步,我們配置 abs() 返回複數的模:

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

我們遵循官方建議的 __repr__() 和 __str__() 兩者差異,用第一個來實現可解析的字元串輸出,用第二個來實現「更美觀」的輸出。 __repr__() 方法簡單地返回 CustomComplex(a, b) 字元串,這樣我們在調用 eval() 重建對象時很方便。 __str__() 方法用來返回帶括弧的複數輸出形式,比例 (a+bj):

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

數學上講,我們可以進行兩個複數相加或者將一個實數和複數相加。我們定義 + 運算符來實現這個功能。方法將會檢測運算符右側的類型,如果是一個整數或者浮點數,它將只增加實部(因為任意實數都可以看做是 a+0j),當類型是複數時,它會同時更改實部和虛部:

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)

同樣,我們定義 -* 運算符的行為:

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)

因為加法和乘法可以交換操作數,我們可以在反向運算符 __radd__() 和 __rmul__() 方法中這樣調用 __add__() 和 __mul__() 。此外,減法運算的操作數是不可以交換的,所以需要 __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)

注意:你也許發現我們並沒有增添一個構造函數來處理 CustomComplex 實例。因為這種情形下,兩個操作數都是類的實例, __rsub__() 方法並不負責處理實際的運算,僅僅是調用 __sub__() 方法來處理。這是一個微妙但是很重要的細節。

下面,我們來看看另外兩個運算符:== 和 != 。這兩個分別對應的特定方法是 __eq__() 和 __ne__()。如果兩個複數的實部和虛部都相同則兩者是相等的。只要兩個部分任意一個不相等兩者就不相等:

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)

注意:浮點指南這篇文章討論了浮點數比較和浮點精度的問題,它涉及到一些浮點數直接比較的一些注意事項,這與我們在這裡要處理的情況有點類似。

同樣,我們也可以通過簡單的公式來提供複數的冪運算。我們通過定義 __pow__() 特定方法來設置內置函數 pow() 和 ** 運算符的行為:

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)

注意:認真看看方法的定義。我們調用 abs() 來獲取複數的模。所以,我們一旦為特定功能函數或運算符定義好了特定方法,它就可以被用於此類的其他方法中。

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

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

字元串表示:

>>> aCustomComplex(1, 2)>>> bCustomComplex(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 + bCustomComplex(4, -2)>>> a - bCustomComplex(-2, 6)>>> a + 5CustomComplex(6, 2)>>> 3 - aCustomComplex(2, -2)>>> a * 6CustomComplex(6, 12)>>> a * (-6)CustomComplex(-6, -12)

相等和不等檢測:

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

最後,複數的冪運算:

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

正如你所見,我們自定義類的對象外觀及行為上類似於內置的對象而且很 Pythonic。

回顧總結

本教程中,你學習了 Python 數據模型,以及如何通過數據模型來構建 Pythonic 的類。學習了改變 len(), abs(), str(), bool() 等內置函數的行為,以及改變 +, -, *, **, 等內置運算符的行為。

如果想要進一步地了解數據模型、函數和運算符重載,請參考以下資源:

  • Python 文檔,數據模型的第 3.3 節,特定方法名
  • 流暢的 Python(Fluent Python by Luciano Ramalho)
  • Python 技巧(Python Tricks)

完整示例代碼:

from math import hypot, atan, sin, cosclass 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)

推薦閱讀:

集智的廣告日常(1)
上線一星期後,來聊聊小程序的「預想之中」和「意料之外」?
個人感想:《On Lisp》和 《DSL》:lisp為什麼不流行?
C語言基礎:多維數組
學遊戲開發需要學什麼

TAG:編程語言 | Python | 編程 |