2020年9月15日星期二

012 零基礎入門學習Python 第12章 魔法方法


申明本站飛宇網 https://feiyetopro.blogspot.com/自網路收集整理之書籍文章影音僅供預覽交流學習研究,其[書籍、文章、影音]情節內容, 評論屬其個人行為, 與本網站無關。版權歸原作者和出版社所有,請在下載 24 小時內刪除,不得用作商業用途;如果您喜歡其作品,請支持訂閱購買[正版]謝謝!




12.1 構造和析構
在此之前,已經接觸過Python最常用的魔法方法,小甲魚也把魔法方法說得神乎其神,似乎用了就可以化腐朽為神奇,化干戈為玉帛,化不可能為可能!
說的這麼厲害,那什麼是魔法方法呢?
魔法方法總是被雙底線包圍,例如__init__()
魔法方法是物件導向的Python的一切,如果你不知道魔法方法,說明你還沒能意識到物件導向的Python的強大。
魔法方法的魔力體現在它們總能夠在適當的時候被調用。

12.1.1 __init__(self[, …])
之前我們討論過__init__()方法,說它相當於其他物件導向程式設計語言的構造方法,也就是類在產生實體成物件的時候首先會調用的一個方法。
有讀者可能會問:有時候在類定義時寫__init__()方法,有時候卻沒有,這是為什麼呢?這是我在論壇中看到的一個問題,我想應該不僅只有一位朋友有疑惑,所以在這裡解釋下:在現實生活中,有一種東西迫使我們去努力拼搏,使我們獲得創造力和生產力,使我們不惜背井離鄉來到一個陌生的城市承受孤獨和寂寞,這個東西就叫需求……嗯,我想我已經很好地回答了這個問題。舉個例子:
這裡需要注意的是,__init__()方法的返回值一定是None,不能是其他:
所以一般在需要進行初始化的時候才重寫__init__()方法,現在大家應該就可以理解造物者的邏輯了。但是你要知道,神之所以是神,是因為他做什麼事都留有一手。其實,這個__init__()並不是產生實體物件時第一個被調用的魔法方法。

12.1.2 __new__(cls[, …])
__new__()才是在一個物件產生實體的時候所調用的第一個方法。它跟其他魔法方法不同,它的第一個參數不是self而是這個類(cls),而其他的參數會直接傳遞給__init__()方法的。
__new__()方法需要返回一個實例物件,通常是cls這個類產生實體的物件,當然你也可以返回其他物件。
__new__()方法平時很少去重寫它,一般讓Python用默認的方案執行就可以了。但是有一種情況需要重寫這個魔法方法,就是當繼承一個不可變的類型的時候,它的特性就顯得尤為重要了。
這裡返回str.__new__(cls, string)這種做法是值得推崇的,只需要重寫我們關注的那部分內容,然後其他的瑣碎東西交給Python的默認機制去完成就可以了,畢竟它們出錯的幾率要比我們自己寫小得多。

12.1.3 __del__(self)
如果說__init__()__new__()方法是物件的構造器的話,那麼Python也提供了一個析構器,叫作__del__()方法。當物件將要被銷毀的時候,這個方法就會被調用。但一定要注意的是,並非del x就相當於自動調用x.__del__()__del__()方法是當垃圾回收機制回收這個物件的時候調用的。舉個例子:

12.2 算數運算
現在來講一個新的名詞:工廠函數,不知道大家還有沒有聽過?其實在老早就一直在使用它,但由於那時候還沒有學習類和物件,我知道那時候說了也是白說。但我知道現在來告訴大家,理解起來就不再是問題了。
Python2.2以後,對類和類型進行了統一,做法就是將int()float()str()list()tuple()這些BIF轉換為工廠函數:
看到沒有,普通的BIF應該是<class'builtin_function_or_method'>,而工廠函數則是<class'type'>。大家有沒有覺得這個<class'type'>很眼熟,在哪裡看過?沒錯啦,如果定義一個類:
它的類型也是type類型,也就是類物件,其實所謂的工廠函數,其實就是一個類物件。當你調用它們的時候,事實上就是創建一個相應的實例物件:
現在你是不是豁然發現:原來物件是可以進行計算的!其實你早該發現這個問題了,Python中無處不物件,當在求a+b等於多少的時候,事實上Python就是在將兩個物件進行相加操作。Python的魔法方法還提供了自訂物件的數值處理,通過對下面這些魔法方法的重寫,可以自訂任何物件間的算數運算。

12.2.1 算術操作符
12-1列舉了算數運算相關的魔法方法。
12-1 算數運算相關的魔法方法
舉個例子,下面定義一個比較特立獨行的類:
那有些讀者可能會問:我想自己寫代碼,不想通過調用Python默認的方案行不行?答案是肯定行,但要格外小心!
為什麼會陷入無限遞迴呢?問題就出在這裡:
當物件涉及加法操作時,自動調用魔法方法__add__(),但看看上邊的魔法方法寫的是什麼?寫的是return self+other,也就是返回物件本身加另外一個物件,這不就又自動觸發調用__add__()方法了嗎?這樣就形成了無限遞迴。所以,像下面這麼寫就不會觸發無限遞迴了:
上邊介紹了很多有關算數運算的魔法方法,意思是當物件進行了相關的算數運算,自然而然就會自動觸發對應的魔法方法。嘿,有悟性的讀者就會說:哇,我似乎感覺到擁有了上帝的力量。沒錯吧?
Python正是如此,對於初學者,他們不知道魔法方法,所以默認的魔法方法會讓他們以合乎邏輯的形式運行。但當你逐步深入學習,慢慢有了沉澱之後,你突然發現如果有更多的靈活性,就可以把程式寫得更好……這時候,Python也可以滿足你。通過對指定魔法方法的重寫,你完全可以讓Python根據你的意願去執行。
當然,我這樣做從邏輯上是說不過去的……我只是想跟大家說,隨著學習的足夠深入,Python允許你做的事情就更多、更靈活!

12.2.2 反運算
12-2列舉了反運算相關的魔法方法。
12-2 反運算相關的魔法方法
不難發現,這裡的反運算魔法方法跟上節介紹的算術運算子保持一一對應,不同之處就是反運算的魔法方法多了一個“r”,例如,__add__()就對應__radd__()。舉個例子:
試一下:
關於反運算,這裡還要注意一點:對於a+bb__radd__(self, other)selfb物件,othera物件。
所以不能這麼寫:
所以對於注重運算元順序的運算子(例如減法、除法、移位),在重寫反運算魔法方法的時候,就一定要注意順序問題了。

12.2.3 增量賦值運算
Python也有大量的魔術方法可以來定制增量設定陳述式,增量賦值其實就是一種偷懶的形式,它將操作符與賦值來結合起來。例如:

12.2.4 一元操作符
一元操作符就是只有一個運算元的意思,像a+b這樣,加號左右有ab兩個運算元,叫作二元操作符。只有一個運算元的,例如把減號放在一個運算元的前邊,就是取這個運算元的相反數的意思,這時候管它叫負號。
Python支持的一元操作符主要有__neg__()(表示正號行為),__pos__()(定義負號行為),__abs__()(定義當被abs()調用時的行為,就是取絕對值的意思),還有一個__invert__()(定義按位取反的行為)。

12.3 簡單定制
基本要求:
定制一個計時器的類。
startstop方法代表啟動計時和停止計時。
假設計時器物件t1print(t1)和直接調用t1均顯示結果。
當計時器未啟動或已經停止計時,調用stop方法會給予溫馨的提示。
兩個計時器物件可以進行相加:t1+t2
只能使用提供的有限資源完成。
這裡需要限定你只能使用哪些資源,因為Python的模組是非常多的,你要是直接上網找個寫好的模組進來,那就達不到鍛煉的目的了。
下邊是演示:
你需要下面的資源:
使用time模組的localtime方法獲取時間(有關time模組可參考:http://bbs.fishc.com/thread-51326-1-1.html)。
time.localtime返回struct_time的時間格式。
表現你的類:__str__()__repr__()魔法方法。
有了這些知識,可以開始來編寫代碼了:
似乎做得不錯了,但這裡還有一些問題。假設用戶不按常理出牌,問題就會很多:
當直接執行t1的時候,Python會調用__str__()魔法方法,但它卻說這個類沒有prompt屬性。prompt屬性在哪裡定義的?在_calc()方法裡定義的,對不?但是沒有執行stop()方法,_calc()方法就沒有被調用到,所以也就沒有prompt屬性的定義了。
要解決這個問題也很簡單,大家應該還記得在類裡邊,用得最多的一個魔法方法是什麼?是__init__()嘛,所有屬於實例物件的變數只要在這裡邊先定義,就不會出現這樣的問題了。
這裡又出錯了(當然我是故意的),大家先檢查一下是什麼問題?
其實會導致這個問題,是因犯了一個微妙的錯誤,這樣的錯誤通常很容易疏忽,而且很難排查。Python這裡拋出了一個異常:TypeError'int'object is not callable
仔細瞧,在調用start()方法的時候報錯,也就是說,Python認為start是一個整型變數,而不是一個方法。為什麼呢?大家看__init__()方法裡,是不是也命名了一個叫作self.start的變數,如果類中的方法名和屬性同名,屬性會覆蓋方法。
好了,讓我們把所有的self.startself.end都改為self.beginself.end吧!
現在程式沒問題了,但顯示時間是000003這樣不大人性化,還是希望可以按照年月日小時分鐘秒這麼去顯示,然後值為0的就不顯示啦,這樣才是人看的嘛,對不對?!所以這裡添加一個清單用來存放對應的單位:
然後在適當的地方增加溫馨提示:
最後,再重寫一個魔法方法__add__(),讓兩個計時器物件相加會自動返回時間的和:
看上去代碼是不錯,也能正常計算了。但是,這個程式有幾點不足還需要大家課後來思考一下如何修改:
1)如果開始計時的時間是(2022222163030),停止時間是(2025123153030),那麼按照用停止時間減開始時間的計算方式就會出現負數,你應該對此做一些轉換。
2)現在的電腦速度都非常快,而這個程式最小的計算單位卻只是秒,精度是遠遠不夠的。

12.4 屬性訪問
通常可以通過點(.)操作符的形式去訪問物件的屬性,在11.9節中也談到了如何通過幾個BIF適當地去訪問屬性:
然後還介紹了一個叫作property()函數的用法,這個property()使得我們可以用屬性去訪問屬性:
那麼關於屬性訪問,肯定也有相應的魔法方法來管理。通過對這些魔法方法的重寫,你可以隨心所欲地控制物件的屬性訪問。大家是不是想想就有點小激動了呢?來吧,讓我們開始吧!
12-3列舉了屬性相關的魔法方法。
12-3 屬性相關的魔法方法
做個小測試:
這幾個魔法方法在使用上需要注意的是,有一個閉環的陷阱,初學者比較容易中招,還是通過一個實例來講解!寫一個矩形類(Rectangle),默認有寬(width)和高(height)兩個屬性;如果為一個叫square的屬性賦值,那麼說明這是一個正方形,值就是正方形的邊長,此時寬和高都應該等於邊長。
這是為什麼呢?
分析一下:產生實體物件,調用__init__()方法,在這裡給self.widthself.heigth分別初始化賦值。一發生賦值操作,就會自動觸發__setattr__()魔法方法,widthheight兩個屬性被賦值,於是執行else的下邊的語句,就又變成了self.width=value,那麼就相當於又觸發了__setattr__()魔法方法了,閉環陷阱就是這麼來的。
那怎麼解決呢?我這裡說兩個方法。第一個就是跟剛才一樣,用super()來調用基類的__setattr__(),那麼這樣就依賴基類的方法來實現賦值:
另一種方法就是給特殊屬性__dict__賦值。物件有一個特殊的屬性,叫作__dict__,它的作用是以字典的形式顯示出當前物件的所有屬性以及相對應的值:

12.5 描述符(property的原理)
此前提到過property()函數,這不提不要緊,一提不得了,把大家的好奇心都給提起來了。大家都在問:property()到底被下了什麼藥?怎麼這麼神奇?如果你想知道property()函數的實現原理,那麼本節的內容就不能錯過。
本節要講的內容叫作描述符(descriptor),用一句話來解釋,描述符就是將某種特殊類型的類的實例指派給另一個類的屬性。那什麼是特殊類型的類呢?就是至少要在這個類裡邊定義__get__()__set__()__delete__()三個特殊方法中的任意一個。
12-4列舉了描述符相關的魔法方法。
12-4 描述符相關的魔法方法
舉個最直觀的例子:
由於MyDescriptor實現了__get__()__set__()__delete__()方法,並且將它的類實例指派給Test類的屬性,所以MyDescriptor就是所謂描述符類。到這裡,大家有沒有看到property()的影子?
好,產生實體Test類,然後嘗試對x屬性進行各種操作,看看描述符類會有怎樣的回應:
當訪問x屬性的時候,Python會自動調用描述符的__get__()方法,幾個參數的內容分別是:self是描述符類自身的實例;instance是這個描述符的擁有者所在的類的實例,在這裡也就是Test類的實例;owner是這個描述符的擁有者所在的類本身。
x屬性進行賦值操作的時候,Python會自動調用__set__()方法,前兩個參數跟__get__()方法是一樣的,最後一個參數value是等號右邊的值。
最後一個del操作也是同樣的道理:
只要弄清楚描述符,那麼property的秘密就不再是秘密了!property事實上就是一個描述符類。下邊就定義一個屬於我們自己的MyProperty
看,這不就自己實現property()函數了嘛,簡單吧?!
最後講一個實例:先定義一個溫度類,然後定義兩個描述符類用於描述攝氏度和華氏度兩個屬性。兩個屬性會自動進行轉換,也就是說,你可以給攝氏度這個屬性賦值,然後列印的華氏度屬性是自動轉換後的結果。

12.6 定制序列
常言道,無規矩不成方圓,講的是萬事萬物的發展都是要在一定的規則下進行,只有遵照一定的協議去做了,事情才能往正確的方向上發展。
本節要談的是定制容器,要想成功地實現容器的定制,便需要先談一談協定。協議是什麼呢?協定(Protocol)與其他程式設計語言中的介面很相似,它規定哪些方法必須要定義。然而,在Python中的協議就顯得不那麼正式。事實上,在Python中,協定更像是一種指南。
這有點像Python極力推崇的鴨子類型(擴展閱讀:http://bbs.fishc.com/thread-51471-1-1.html),當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這只鳥就可以被稱為鴨子。Python就是這樣,並不會嚴格地要求你一定要怎樣去做,而是讓你靠著自覺和經驗把事情做好!
Python中,像序列類型(如清單、元組、字串)或映射類型(如字典)都是屬於容器類型。本節來講定制容器,那就必須要知道,定制容器有關的一些協議:
如果說你希望定制的容器是不可變的話,你只需要定義__len__()__getitem__()方法。
如果你希望定制的容器是可變的話,除了__len__()__getitem__()方法,你還需要定義__setitem__()__delitem__()兩個方法。
12-5列舉了定制容器類型相關的魔法方法及含義。
12-5 定制容器類型相關的魔法方法
驗證大家學習能力的時候到了。現在動動手,編寫一個不可改變的自訂清單,要求記錄清單中每個元素被訪問的次數。

12.7 反覆運算器
自始至終,有一個概念一直在用,但我們卻從來沒有認真地去深入剖析它。這個概念就是反覆運算。反覆運算這個詞聽得很多了,現在不僅在數學領域使用這個詞,我們經常聽到類似這個產品經過多次反覆運算,品質和品質已經有了大幅度提高,這次事件純屬意外……
大家應該聽出來了,反覆運算的意思類似於迴圈,每一次重複的過程被稱為一次反覆運算的過程,而每一次反覆運算得到的結果會被用來作為下一次反覆運算的初始值。提供反覆運算方法的容器稱為反覆運算器,通常接觸的反覆運算器有序列(清單、元組、字串)還有字典也是反覆運算器,都支持反覆運算的操作。
舉個例子,通常使用for語句來進行反覆運算:
字串就是一個容器,同時也是一個反覆運算器,for語句的作用就是觸發這個反覆運算器的反覆運算功能,每次從容器裡依次拿出一個資料,這就是反覆運算操作。
字典和檔也是支援反覆運算操作的:
關於反覆運算,Python提供了兩個BIF
iter()
next()
對一個容器物件調用iter()就得到它的反覆運算器,調用next()反覆運算器就會返回下一個值,然後怎麼樣結束呢?如果反覆運算器沒有值可以返回了,Python會拋出一個叫作StopIteration的異常:
所以,利用這兩個BIF,可以分析出for語句其實是這麼工作的:
那麼關於實現反覆運算器的魔法方法有兩個:
__iter()__
__next()__
一個容器如果是反覆運算器,那就必須實現__iter__()魔法方法,這個方法實際上就是返回反覆運算器本身。接下來重點要實現的是__next__()魔法方法,因為它決定了反覆運算的規則。簡單舉個例子大家就清楚了:
好了,這個反覆運算器的唯一亮點就是沒有終點,所以如果沒有跳出迴圈,它會不斷反覆運算下去。那可不可以加一個參數,用於控制反覆運算的範圍呢?
是不是很容易呢?嗯,Python就是可以這麼簡簡單單的一門語言!

12.8 生成器(亂入)
由於前邊介紹了反覆運算器,所以這裡趁熱打鐵,給大家講一講這個生成器。雖然說生成器和反覆運算器可以說是Python近幾年來引入的最強大的兩個特性,但是生成器的學習,並不涉及魔法方法,甚至它巧妙地避開了類和物件,僅通過普通的函數就可以實現了。
由於生成器的概念要比較高級一些,所以在函數章節就沒有提及它,還是那句老話,因為那時候講了也是白講。學習就是這麼一個漸進的過程,像上節介紹的反覆運算器,很多人學完之後感歎:哎呀,Python怎麼就這麼簡單呐!但如果在講迴圈那個章節來講反覆運算器的實現原理,那大家勢必就會一頭霧水了。
正如剛才說的,生成器其實是反覆運算器的一種實現,那既然反覆運算可以實現,為何還要生成器呢?有一句話叫存在即合理,生成器的發明一方面是為了使得Python更為簡潔,因為,反覆運算器需要我們自己去定義一個類和實現相關的方法,而生成器則只需要在普通的函數中加上一個yield語句即可。
在另一個更重要的方面,生成器的發明,使得Python模仿協同程式的概念得以實現。所謂協同程式,就是可以運行的獨立函式呼叫,函數可以暫停或者掛起,並在需要的時候從程式離開的地方繼續或者重新開始。
對於調用一個普通的Python函數,一般是從函數的第一行代碼開始執行,結束於return語句、異常或者函數所有語句執行完畢。一旦函數將控制權交還給調用者,就意味著全部結束。函數中做的所有工作以及保存在區域變數中的資料都將丟失。再次調用這個函數時,一切都將從頭創建。
Python是通過生成器來實現類似於協同程式的概念:生成器可以暫時掛起函數,並保留函數的區域變數等資料,然後在再次調用它的時候,從上次暫停的位置繼續執行下去。
好,多說不如實幹,舉個例子:
正如大家所看到的,當函數結束時,一個StopIteration異常就會被拋出。由於Pythonfor迴圈會自動調用next()方法和處理StopIteration異常,所以for迴圈當然也是可以對生成器產生作用的:
像前面介紹的斐波那契的例子,也可以用生成器來實現:
事到如今,你應該已經很好地掌握了列表推導式,那大家猜猜看下邊這個列表推導式表達的是啥意思:
其實上邊這個列表推導式求的就是100以內,能夠被2整除,但不能夠被3整除的所有整數:
Python3除了有清單推導式之外,還有字典推導式:
還有集合推導式:
那這時有讀者可能就會想:那按照這種劇情發展下去,應該會有字串推導式和元組推導式吧?不妨試試:
噢,不行,因為只要在雙引號內,所有的東西都變成了字串,所以不存在字串推導式了。那元組推導式呢?
咦?似乎這個不是什麼推導式,大家看出來什麼門道了嗎?generator,多麼熟悉的單詞啊,不就是生成器嘛!沒錯,用普通的小括弧括起來的正是生成器推導式,來證明一下:
for語句把剩下的都列印出來:
還有一點特性更牛,生成器推導式如果作為函數的參數,可以直接寫推導式,而不用加小括弧:
關於生成器的技術要點,這裡小甲魚還給大家轉了一篇不錯的文章,大家課後可以參考學習一下:解釋yieldGenerators(生成器)(http://bbs.fishc.com/thread-56023-1-1.html)。


0 留言:

發佈留言