全網最適合入門的物件導向程式設計教程:44 Python內建函式與魔法方法-重寫內建型別的魔法方法

FreakStudio發表於2024-09-03

全網最適合入門的物件導向程式設計教程:44 Python 內建函式與魔法方法-重寫內建型別的魔法方法

image

摘要:

在 Python 中,內建型別的行為是透過一組特殊的“魔法方法”來實現的,這些魔法方法以雙下劃線開頭和結尾,比如 init 和 str,你可以透過重寫這些魔法方法來定製或擴充套件內建型別的行為。

原文連結:

FreakStudio的部落格

往期推薦:

學嵌入式的你,還不會物件導向??!

全網最適合入門的物件導向程式設計教程: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 平行計算

一文搞懂 CM3 微控制器除錯原理

肝了半個月,嵌入式技術棧大彙總出爐

電子計算機類比賽的“武林秘籍”

一個MicroPython的開源專案集錦:awesome-micropython,包含各個方面的Micropython工具庫

Avnet ZUBoard 1CG開發板—深度學習新選擇

SenseCraft 部署模型到Grove Vision AI V2影像處理模組

文件和程式碼獲取:

可訪問如下連結進行對文件下載:

https://github.com/leezisheng/Doc

image

本文件主要介紹如何使用 Python 進行物件導向程式設計,需要讀者對 Python 語法和微控制器開發具有基本瞭解。相比其他講解 Python 物件導向程式設計的部落格或書籍而言,本文件更加詳細、側重於嵌入式上位機應用,以上位機和下位機的常見串列埠資料收發、資料處理、動態圖繪製等為應用例項,同時使用 Sourcetrail 程式碼軟體對程式碼進行視覺化閱讀便於讀者理解。

相關示例程式碼獲取連結如下:https://github.com/leezisheng/Python-OOP-Demo

正文

Python 內建函式與魔法方法

Python 中有許多函式可以針對特定型別的物件執行某些任務或計算結果,這些函式不需要是某些底層類的方法。它們通常是抽象出來的一些常用計算,可以應用於多種型別的類。這些函式統稱內建函式,我們往往使用內建函式呼叫物件的魔法方法,所謂魔法方法(Magic Method)是 python 內建方法,以兩個下劃線開頭、兩個下劃線結尾,它不需要主動呼叫,存在的目的是為了給 python 的直譯器進行呼叫。常見內建函式有以下這些:

image

最簡單的例子就是 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__))

輸出如下:

image

實際上,python 中常見的魔法方法大致可分為以下幾類:構造與初始化、類的表示、運算子、訪問控制、容器類操作、可呼叫物件、序列化操作等。我們在下圖中對常用魔法方法進行了總結:

image

image

image

image

image

image

image

image

image

重寫內建型別的魔法方法

重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。重寫的好處在於子類可以根據需要,定義特定於自己的行為。

這裡以魔法方法中的__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)

如下為執行結果,儘管這個例子沒有什麼實際應用的例子,但還是能很好的講述如何對算數運算子方法進行重寫:

image

重寫後的__add__方法可以新增到任何我們自己寫的類中,如果我對這個類的例項使用 + 運算子,將會呼叫__add__。例如,字串、元組以及列表的連線就是這麼實現的。所有的特殊方法都是這樣的。如果想要對自定義的物件使用 x in myobj 語法,可以實現__contains__方法。如果想要用 myobj[i] = value 語法,只需要提供__setitem__方法。如果想用 something = myobj[i],需要實現__getitem__。

image

相關文章