在 Python 中,內置類型的行為是通過一組特殊的“魔法方法”來實現的,這些魔法方法以雙下劃線開頭和結尾,比如 init 和 str,你可以通過重寫這些魔法方法來定製或擴展內置類型的行為。 ...
全網最適合入門的面向對象編程教程:44 Python 內置函數與魔法方法-重寫內置類型的魔法方法
摘要:
在 Python 中,內置類型的行為是通過一組特殊的“魔法方法”來實現的,這些魔法方法以雙下劃線開頭和結尾,比如 init 和 str,你可以通過重寫這些魔法方法來定製或擴展內置類型的行為。
原文鏈接:
往期推薦:
全網最適合入門的面向對象編程教程:00 面向對象設計方法導論
全網最適合入門的面向對象編程教程:01 面向對象編程的基本概念
全網最適合入門的面向對象編程教程:02 類和對象的 Python 實現-使用 Python 創建類
全網最適合入門的面向對象編程教程:03 類和對象的 Python 實現-為自定義類添加屬性
全網最適合入門的面向對象編程教程:04 類和對象的Python實現-為自定義類添加方法
全網最適合入門的面向對象編程教程:05 類和對象的Python實現-PyCharm代碼標簽
全網最適合入門的面向對象編程教程:06 類和對象的Python實現-自定義類的數據封裝
全網最適合入門的面向對象編程教程:07 類和對象的Python實現-類型註解
全網最適合入門的面向對象編程教程:08 類和對象的Python實現-@property裝飾器
全網最適合入門的面向對象編程教程:09 類和對象的Python實現-類之間的關係
全網最適合入門的面向對象編程教程:10 類和對象的Python實現-類的繼承和里氏替換原則
全網最適合入門的面向對象編程教程:11 類和對象的Python實現-子類調用父類方法
全網最適合入門的面向對象編程教程:12 類和對象的Python實現-Python使用logging模塊輸出程式運行日誌
全網最適合入門的面向對象編程教程:13 類和對象的Python實現-可視化閱讀代碼神器Sourcetrail的安裝使用
全網最適合入門的面向對象編程教程:全網最適合入門的面向對象編程教程:14 類和對象的Python實現-類的靜態方法和類方法
全網最適合入門的面向對象編程教程:15 類和對象的 Python 實現-__slots__魔法方法
全網最適合入門的面向對象編程教程:16 類和對象的Python實現-多態、方法重寫與開閉原則
全網最適合入門的面向對象編程教程:17 類和對象的Python實現-鴨子類型與“file-like object“
全網最適合入門的面向對象編程教程:18 類和對象的Python實現-多重繼承與PyQtGraph串口數據繪製曲線圖
全網最適合入門的面向對象編程教程:19 類和對象的 Python 實現-使用 PyCharm 自動生成文件註釋和函數註釋
全網最適合入門的面向對象編程教程:20 類和對象的Python實現-組合關係的實現與CSV文件保存
全網最適合入門的面向對象編程教程:21 類和對象的Python實現-多文件的組織:模塊module和包package
全網最適合入門的面向對象編程教程:22 類和對象的Python實現-異常和語法錯誤
全網最適合入門的面向對象編程教程:23 類和對象的Python實現-拋出異常
全網最適合入門的面向對象編程教程:24 類和對象的Python實現-異常的捕獲與處理
全網最適合入門的面向對象編程教程:25 類和對象的Python實現-Python判斷輸入數據類型
全網最適合入門的面向對象編程教程:26 類和對象的Python實現-上下文管理器和with語句
全網最適合入門的面向對象編程教程:27 類和對象的Python實現-Python中異常層級與自定義異常類的實現
全網最適合入門的面向對象編程教程:28 類和對象的Python實現-Python編程原則、哲學和規範大彙總
全網最適合入門的面向對象編程教程:29 類和對象的Python實現-斷言與防禦性編程和help函數的使用
全網最適合入門的面向對象編程教程:30 Python的內置數據類型-object根類
全網最適合入門的面向對象編程教程:31 Python的內置數據類型-對象Object和類型Type
全網最適合入門的面向對象編程教程:32 Python的內置數據類型-類Class和實例Instance
全網最適合入門的面向對象編程教程:33 Python的內置數據類型-對象Object和類型Type的關係
全網最適合入門的面向對象編程教程:34 Python的內置數據類型-Python常用複合數據類型:元組和命名元組
全網最適合入門的面向對象編程教程:35 Python的內置數據類型-文檔字元串和__doc__屬性
全網最適合入門的面向對象編程教程:36 Python的內置數據類型-字典
全網最適合入門的面向對象編程教程:37 Python常用複合數據類型-列表和列表推導式
全網最適合入門的面向對象編程教程:38 Python常用複合數據類型-使用列表實現堆棧、隊列和雙端隊列
全網最適合入門的面向對象編程教程:39 Python常用複合數據類型-集合
全網最適合入門的面向對象編程教程:40 Python常用複合數據類型-枚舉和enum模塊的使用
全網最適合入門的面向對象編程教程:41 Python常用複合數據類型-隊列(FIFO、LIFO、優先順序隊列、雙端隊列和環形隊列)
全網最適合入門的面向對象編程教程:42 Python常用複合數據類型-collections容器數據類型
全網最適合入門的面向對象編程教程:43 Python常用複合數據類型-擴展內置數據類型
更多精彩內容可看:
給你的 Python 加加速:一文速通 Python 並行計算
一個MicroPython的開源項目集錦:awesome-micropython,包含各個方面的Micropython工具庫
SenseCraft 部署模型到Grove Vision AI V2圖像處理模塊
文檔和代碼獲取:
可訪問如下鏈接進行對文檔下載:
https://github.com/leezisheng/Doc
本文檔主要介紹如何使用 Python 進行面向對象編程,需要讀者對 Python 語法和單片機開發具有基本瞭解。相比其他講解 Python 面向對象編程的博客或書籍而言,本文檔更加詳細、側重於嵌入式上位機應用,以上位機和下位機的常見串口數據收發、數據處理、動態圖繪製等為應用實例,同時使用 Sourcetrail 代碼軟體對代碼進行可視化閱讀便於讀者理解。
相關示例代碼獲取鏈接如下:https://github.com/leezisheng/Python-OOP-Demo
正文
Python 內置函數與魔法方法
Python 中有許多函數可以針對特定類型的對象執行某些任務或計算結果,這些函數不需要是某些底層類的方法。它們通常是抽象出來的一些常用計算,可以應用於多種類型的類。這些函數統稱內置函數,我們往往使用內置函數調用對象的魔法方法,所謂魔法方法(Magic Method)是 python 內置方法,以兩個下劃線開頭、兩個下劃線結尾,它不需要主動調用,存在的目的是為了給 python 的解釋器進行調用。常見內置函數有以下這些:
最簡單的例子就是 len()內置函數,它可以數出某種容器對象中的項目數,例如字典或列表。實際上,列表對象並沒有 len 長度屬性,你可能不相信,那就讓我們查看一下列表 List 的屬性值。輸入如下代碼:
print(list.__dict__)
我們來看一下輸出:
{'__new__': <built-in method __new__ of type object at 0x00007FF9D4D5B6D0>,
'__repr__': <slot wrapper '__repr__' of 'list' objects>, '__hash__': None,
'__getattribute__': <slot wrapper '__getattribute__' of 'list' objects>,
'__lt__': <slot wrapper '__lt__' of 'list' objects>, '__le__': <slot wrapper
'__le__' of 'list' objects>, '__eq__': <slot wrapper '__eq__' of 'list' objects>,
'__ne__': <slot wrapper '__ne__' of 'list' objects>,
'__gt__': <slot wrapper '__gt__' of 'list' objects>, '__ge__': <slot wrapper '__ge__' of 'list' objects>, '__iter__': <slot wrapper '__iter__' of 'list' objects>,
'__init__': <slot wrapper '__init__' of 'list' objects>, '__len__': <slot wrapper '__len__' of 'list' objects>,
'__getitem__': <method '__getitem__' of 'list' objects>, '__setitem__': <slot wrapper '__setitem__' of 'list' objects>,
'__delitem__': <slot wrapper '__delitem__' of 'list' objects>, '__add__': <slot wrapper '__add__' of 'list' objects>,
'__mul__': <slot wrapper '__mul__' of 'list' objects>, '__rmul__': <slot wrapper '__rmul__' of 'list' objects>,
'__contains__': <slot wrapper '__contains__' of 'list' objects>, '__iadd__': <slot wrapper '__iadd__' of 'list' objects>, '__imul__': <slot wrapper '__imul__' of 'list' objects>,
'__reversed__': <method '__reversed__' of 'list' objects>, '__sizeof__': <method '__sizeof__' of 'list' objects>,
'clear': <method 'clear' of 'list' objects>, 'copy': <method 'copy' of 'list' objects>, 'append': <method 'append' of 'list' objects>,
'insert': <method 'insert' of 'list' objects>, 'extend': <method 'extend' of 'list' objects>, 'pop': <method 'pop' of 'list' objects>,
'remove': <method 'remove' of 'list' objects>, 'index': <method 'index' of 'list' objects>, 'count': <method 'count' of 'list' objects>,
'reverse': <method 'reverse' of 'list' objects>, 'sort': <method 'sort' of 'list' objects>, '__class_getitem__': <method '__class_getitem__' of 'list' objects>,
'__doc__': 'Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument m
我丟,還真沒有,這是怎麼回事?那為什麼我們用 len()函數居然能得到列表的長度?從理論上來說,列表是有長度屬性的。len()應用到的大部分對象都有一個被稱為 len()的方法,其返回同樣的值。因此 len(list)就是調用 list.len()(len()為內置函數、len 為魔法方法) 。
那麼為什麼不用__len__方法而要用 len()函數?
很明顯 len 是一個特殊的雙下畫線方法,這意味著我們不應該直接調用它,主要的原因是效率,利用內置函數調用魔法方法不僅提供額外的服務,而且解釋器做了優化,會比調用方法更快。通過調用 len()函數,我們可以直接訪問 Python 可變長度容器的底層 C 語言中 PyVarObject 結構體的 ob_size 欄位,該欄位保存著容器中的項數。 len(my_object)直接讀取 ob_size 欄位的值,這比調用 len 方法快很多。
另一個原因是可維護性。Python 開發者可能會在未來修改 len()來計算沒有 len 方法的對象的長度,例如,通過計數迭代器返回的項目。那麼他們只需要修改一個函數而不是所有的 len 方法。
除了__len__的例子外,最常見的例子就是要拿到一個列表的某個元素,可以使用對應的引索進行取值,比如 list[key],這背後利用的是__getitem__方法,為了拿到 my_list[key]的值,解釋器實際上會調用 my_list.getitem(key)。
那麼如何得到 Python 內置數據類型的這些特殊方法呢?非常簡單,只需要應用 dir([object])函數即可,dir()函數可以返回對象的屬性、方法列表。以 List 列表類為例,我們只需要輸入以下語句即可:
print(dir(list))
輸出如下:
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__',
'__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__',
'__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear',
'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
此外,如果想要知道如何使用這些方法,可以用 help 函數:
print(help(list.__le__))
輸出如下:
實際上,python 中常見的魔法方法大致可分為以下幾類:構造與初始化、類的表示、操作符、訪問控制、容器類操作、可調用對象、序列化操作等。我們在下圖中對常用魔法方法進行了總結:
重寫內置類型的魔法方法
重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。重寫的好處在於子類可以根據需要,定義特定於自己的行為。
這裡以魔法方法中的__repr__方法和__ str__方法講解如何重寫內置類型的魔法方法。
所謂 repr 是 Python 類中的一個特殊方法,由 object 對象提供,由於所有類都是 object 類的子類,所以都會繼承該方。它是一個”自我描述“的方法,此方法通常實現這樣的功能:當直接列印類的實例化對象時,系統將會輸出對象的自我描述信息,用來告訴外界對象具有的狀態信息。通常情況下,直接輸出某個實例化對象,本意往往是想瞭解該對象的基本信息,例如該對象有哪些屬性,它們的值各是多少等等。但是由於 object 提供的這個 repr 方法總是返回一個對象,(類名 + obejct at + 記憶體地址),這個值並不能真正實現自我描述的功能。因此,如果你想在自定義類中實現“自我描述” 的功能,那麼必須重寫 repr 方法。
與 repr 類似的是__ str__方法,repr 和 str 這兩個方法都是用於顯示的,str 是面向用戶的,而 repr 面向程式員:
- 列印操作會首先嘗試 str 和 str 內置函數(print 運行的內部等價形式),它通常應該返回一個友好的顯示;
- repr 用於所有其他的環境中:用於交互模式下提示回應以及 repr 函數,如果沒有使用 str,會使用 print 和 str。它通常應該返回一個編碼字元串,可以用來重新創建對象,或者給開發者詳細的顯示。
同時,我們也可以對算數操作符進行重寫,在上述列表實現集合的例子中,如下代碼就是對算數操作符進行重寫:
_# 運算符&重載,求交集_
def __and__(self, other):
return self.intersect(other)
_# 運算符|重載,求合集_
def __or__(self, other):
return self.union(other)
_# 當直接列印類的實例化對象時,系統將會輸出對象的自我描述信息_
def __repr__(self):
return 'Set:' + repr(self.data)
再列舉一個有趣的例子,對算數操作符方法重寫進行說明,以下代碼創建一個特殊的整數,每當將兩個這種整數相加時都會返回 0:
class SillyInt(int):
_# 重寫__add__方法_
def __add__(self, num):
return 0
a = SillyInt(1)
b = SillyInt(2)
print(a+b)
如下為運行結果,儘管這個例子沒有什麼實際應用的例子,但還是能很好的講述如何對算數操作符方法進行重寫:
重寫後的__add__方法可以添加到任何我們自己寫的類中,如果我對這個類的實例使用 + 操作符,將會調用__add__。例如,字元串、元組以及列表的連接就是這麼實現的。所有的特殊方法都是這樣的。如果想要對自定義的對象使用 x in myobj 語法,可以實現__contains__方法。如果想要用 myobj[i] = value 語法,只需要提供__setitem__方法。如果想用 something = myobj[i],需要實現__getitem__。