手把手教你學python第十五講(魔法方法續私人“定製”)

手把手教你學python第十五講(魔法方法續私人“定製”)

如果看不了圖,請去https://www.bilibili.com/read/cv317161

python無處不物件的深刻理解

前面寫了這麼多,我覺得有必要從一個大的層面,也就是OO來看問題的本質。只要你呼叫物件的語法是合乎python的習慣的,那就是可以的,我們以前從來沒有像下面這麼寫過,對吧,但是仔細想想有何不可呢?一個類定義完了就是一個物件啊,我當然是可以改變一個物件的屬性,只要語法結構合乎python的習慣,我們下面就是把方法當作普通函式去呼叫,只是因為它是類定義裡的函式,所以前面加上a.b(a),這個.就是python的一種規定,或者說開發人員的原始碼就是這麼寫的,其它的和一般的函式呼叫沒有什麼區別。大家不要學死了,python無處不物件,一定要記住這句話。

t

例項化物件的繫結方法的呼叫只是有一種簡便書寫格式而已,以下圖為例,a1.b()其實相當於從a1.__class__.__mro__裡面去找b方法,並且把a1作為引數傳遞給方法。

我們甚至還可以這麼寫,我用通用寫法b.c(a1),都是沒有問題的,其實self只是個形式引數而已,你可以用任何合法字元,寫成self算是一種規定吧。結合上面理解一下,下面還會講這個問題

繼承的本質

前面我們曾經說過繼承,都是各種覆蓋啊,什麼東西,下面我會以一種視覺化的方法,一種更為簡單的方法讓你來理解繼承。我相信會對你有幫助的

其實訪問d1.n就相當於下面的程式碼

所以說繼承的本質是什麼?拿上面例子來說,其實就是新建一個d1.__class__.__mro__列表,訪問例項化物件的屬性的時候python會先在d1.__dict__找,然後在這個MRO列表裡依次尋找.__dict__有沒有你要找的屬性,沒有報錯,有的話就返回第一個找到的屬性。所以現在可以把腦裡那些什麼各種繼承的屬性標籤的覆蓋都忘了,就按照這個順序來找,我們就再來說說這個__dict__,它其實就是一個字典存放與你這個物件相關的一些東西,物件是類物件的話就是類定義裡定義的屬性和方法,如果是例項化物件就是繫結屬性,實力化物件是沒有方法的,方法都是歸屬於類的,這個前面也說過,我們就再用這個__dict__去理解一下屬性和方法覆蓋的問題,注意看下面__dict__的不同

我們用__dict__來理解繼承的機制

b繼承了a其實相當於什麼?我的理解是b繼承a其實就像我們在b網站上有一個a網站的連結,我們就把__dict__理解為網頁上的內容,b網站可以有和a網站上一樣的板塊,比如說python版塊,但是如果我們在b網站上沒有找到我們想要的版塊(這是可能的吧),這時候就體現了繼承,我們點開b網站裡a網站的連結去到a網站去看有沒有,如果還沒有就繼續點連結,__mro__其實就是連結的一個順序,當然我們這裡假設每一個網站只能有一個連結向其它的網站,假設b網站沒有python版塊,我們就要點開連結去a網站看有沒有,如果有我們實際上是在a網站看的python。如果b網站原來有c語言版塊,我們每天都在b網站看,某一天突然這個版塊被刪了,那麼我們就只能點開連結去a網站看了,a沒有就繼續點開連結。我們再來理解一下例項化的過程,其實例項化是什麼呢?前面說過類就是圖紙,類的例項化就是照著圖紙去蓋房子,現在我們要說了這個房子其實是個假房子,為什麼這麼說呢?就看下面的例子,因為a1作為a的例項化物件以後,a1.__dict__是空的,但是為什麼訪問a1.n返回了1也就是a.n呢?其實這個上面已經講過,就是按照__dict__和__mro__的一個順序去搜尋,這裡我找不到很好的比喻。我們只有發生了賦值行為,就相當於進行了裝修,__dict__裡才會有內容,繫結方法其實就是這麼一種賦值行為。當然python裡的賦值和c語言的賦值含義是不一樣的,學python的有時候會說這麼一句話,python沒有變數只有名字,關於這部分內容前面已經介紹過不少了,包括淺拷貝和深拷貝的內容,不熟悉的請去前面看。

不知道你們還記得不記得前面提到過一個靜態變數,回顧一下

為什麼刪除了a,我們還可以呼叫a1.n呢?前面我們有一種解釋是還有a1.n這個地址指標指向它對吧,但是結合這裡我們認識到本質其實還是按照靜態變數來理解,靜態變數的壽命是程式的全週期,即開啟到關閉IDLE或者其它編譯器的時間,編譯器不關閉,它就一直佔用記憶體,當然還是說你覺得怎麼簡單怎麼理解。上面好像沒有涉及到方法的呼叫,只是說屬性是按照__mro__的順序,其實方法也是,只是第一步不需要搜尋例項化物件的__dict__而已。

沒錯你看到了c.b顯示的是function a.b如果你理解了上面的講解不難理解。

我們重新分析一下a1 5和5 a1結果不同的原因,首先這種雙目運算子,先去找兩邊物件的最底層的類,上面的例子也就是a,上一講已經說過 是自動先呼叫__add__方法的,如果__add__不適用,什麼叫做不適用呢?其實就是isinstance(a1,a)是False,就__radd__方法,a1 5,a1是a的例項化物件,所以按照a.__add__去找,a.__dict__裡面沒有__add__,於是就到a.__mro__的下一個int去找,呼叫了int.__add__。那麼5 a1呢?isinstance(5,a)返回的是False,而且a.__dict__裡面有__radd__,於是就呼叫了。

總結一下這種自動呼叫是這樣的,先判斷isinstance的真假。決定是呼叫__add__還是__radd__,確定了之後就開始呼叫,如果不是自動呼叫就沒有isinstance這個過程了,如下

我們這裡再來理解一下例項化物件繫結方法的簡便寫法的實質,b1.b()其實就當於b1.__class__.b(b1)

其實這種簡寫的本質就是在記憶體裡給這中符合python書寫語法的函式開闢了一個新的記憶體空間來存放這種繫結方法,每例項化一個物件就開闢一個空間。和上面是不衝突的,只是這是一種特殊的寫法而已。

那麼我們來看關於上一講的最後一種super(basetype,type),它是可以呼叫繫結方法的

只需要在後面傳進去引數。還有一點需要注意,用super呼叫父類方法和直接呼叫父類方法的寫法是不同的,注意super函式self引數的位置

為什麼會報錯呢?其實仔細想一下就會發現,self已經在前面給過了,所以報錯說期望得到一個引數,但是給了2個,我們改一下就ok了。

關於屬性的魔法方法

也就是說a1.n就相當於先後呼叫a.__getattrribute__和a.__getattr__,因為是先列印1,再列印0,a1.n=2就相當於自動呼叫a.__setattr__(a1,n,2),del a1.n就相當於呼叫a.__delattr__(a1,n)。我這裡要提醒一點,防止出現無限遞迴,也就是不要出現這種程式

其實想一想就知道就知道為什麼,a1.n=2本身就是呼叫__setattr__方法本身,當然是無窮遞迴咯。我們來稍微寫一個小程式吧,來定義一個矩形類,如果給屬性square賦值,那麼它就自動的讓長等於寬,如果不是,就輸入長和寬

程式本身不難,就是體會一屬性魔法方法的使用

這裡我還要講一點,雖然以前我已經講過這點了

報錯的原因是super沒有屬性c,是不是讓你以為是super 函式的問題?但是其實不是的

super沒有__add__方法為什麼不報錯?其實這裡應該是super()的問題,對於報錯的其實就是object的問題,因為a.__mro__裡a後面就是object,我們看object是沒有__getattr__方法的,而super(a)其實就是int,int是有__add__方法的,這點希望你們不要誤解。不知道你們還記得不記得我們前面還學過一些函式來得到屬性的

其實它們也是在內部呼叫了這些魔法方法。是不是還有一個呢?沒錯還有一個property,下面參考了http://bbs.fishc.com/thread-51106-1-1.html,但是有我自己新的理解

,沒錯,property是一個類,它並不是一個函式

這種類我們稱之為描述符,什麼是描述符?某種型別是說有下面三種魔法方法的類

說詳細一點的話

我們先來看看這三個魔法方法,首先要說明的是這三個魔法方法不像__new__,__init__這種每個類都會有,而只是特殊的描述類才會有,比如說property,當然我們可以自己寫這些魔法方法

我們再來掌握一下這些魔法方法各個位置的引數是什麼含義。看下圖我們很簡單就可以理解為什麼說描述符是將某種型別的例項物件指派給另一種類的屬性。因為b類裡我們看到了n=b(),n就是b的一個屬性,而a()是a類的一個例項化物件,而b類的屬性n就作為標籤或者說指標指向a()。然後我們呼叫了b.n,這時候其實自動呼叫了a類裡的__get__,看到先後列印了a的例項物件,None,b類。說明self就是a()也就是b.n,instance是None,owner是b。這是直接呼叫,然後我們例項化了b類,產生了b1例項化物件,下一行給b1的屬性s賦值2,並沒有發生任何事情,因為屬性並不是n。接下來給b1的屬性n賦值3,自動呼叫a裡的__set__,列印了a的例項物件,b的物件,也就是說self是a(),也就是b1.n,instance是b1,其實到這裡我們能看出些端倪了,如果沒感覺,沒關係,下面我們又訪問了b1.n,自動呼叫了a.__get__,列印了b.n,b1,3。我來解釋一下,其實b1.n=3程式碼相當於a.__set__(b1.n,b1,3),在這個__set__裡我們改變了self.value也就是a().value的值,然後b1.n就相當於

a.__get__(b1.n,b1,b),注意b.n=a(),你應該知道對應關係是怎樣的了。為什麼上面的b.n

第二個是None呢?很簡單因為沒有b的例項物件。還有下面b.n=3之後為什麼什麼都不會發生了呢?因為b.n已經不指向a()了嘛,後面的程式碼都很好理解,只要你看了前面所有的系列。

這幾個引數不要隨便變都是有固定的格式的

上面為什麼在括號裡寫b1.n而不寫a(),這是因為寫a()的指標可以避免下面的問題

沒有指標指向例項物件,每次a()都相當於創造了一個新的例項物件,雖然它們很快就會被垃圾回收機制回收,但是id還是不一樣的,但是你看到上面的例子裡a的object的地址全是0x013374D,所以直接寫指標b.n。但是不知道針對這點你們會不會想到什麼問題,既然b.n是一樣的,那麼如果我有b的兩個例項物件b1,b2,我先b1.n=2,在b2.n=3,b1.n會變嗎?

看來我們會死杞人憂天了,其實仔細想想也能理解,因為還有一個引數instance呢,另外從上面還可以看到,b1.n與b.n都是在0x019A76F0,b2.n是列印0x019A7390,變了對吧,這也可以理解為什麼會出現上面的結果,每一個b的例項物件建立出來,a()都會執行一次,也就是說會有一個新的a例項物件,就拿上面來說0x019A76F0是和b.n和b1.n有關的,而0x019A7390是和b2.n有關。還有b的第一個例項物件b1.n和b.n指向的地址是一個地方,並且我們讓b1.n=2,b.n的值跟著被改變了。我現在想刪掉b1.n,唉報錯了,因為我們沒有定義a.__delete__,刪除b.n為什麼可以?因為它只是b的一個屬性而已對吧,完全沒有問題,但是為什麼b1.n報錯,就是因為這裡面a是描述符,而del b1.n就是會自動呼叫a.__delete__但是我們沒有定義,所以報錯,注意上面說過除了property有這個魔法方法以外,你自己定義的類的__delete__需要你自己去寫。

如果你前面學的融匯貫通了,下面程式碼你會一看就懂是怎麼回事,就是在類定義外面增加了屬性而已

需要注意的是這個屬性必須是類層次的屬性,不能是某個例項物件的繫結屬性。

至於為什麼會出現這樣,我不是很瞭解,畢竟我們連__get__,__set__,__delete__三個魔法方法的原始碼都不知道,我們只是會用。但是其實有這種騷操作

這種操作其實每次例項化一次b,b.n=a()都會執行一次,也就是會說每例項化一次,b.n都會隨著例項物件變一次,這就是b2.n=3之後b.n也等於3的原因,你可以去和上面在類層面上的屬性對比,最後的結果是b.n=2。體會這種不同。我們來試著編寫一個小程式體會一下這種程式設計思想。

我覺得這個程式你們應該嘗試自己去讀懂,為什麼有的是self有的是instance?一定要自己讀懂,我在每個魔法方法裡都加了一些標記,這會幫助你讀懂它,我不會講解了,我希望你能思考讓腦子動起來。

說了這麼多我們是不是還沒說到property。這裡就是,參考了http://bbs.fishc.com/thread-51106-1-1.html。裡面說的是有錯的,首先上面我們就知道property並不是個bif,嚴格來說是一個類。再來看一遍property的幫助

我沒有太多專案經驗,對它的理解不是很到位,所以這裡摘一段話解釋property的作用

上面已經有個典型例子。細心的人會發現為什麼x前面要加一個_x?只是為了和x區分而已,這裡的類屬性和繫結屬性不能重名的,不然會無限遞迴

為什麼會無限遞迴,其實也很好理解,就是因為重名的問題,a1.n=4我們進入了a.b(a1,4)

裡面又是a1.n=4,又呼叫了a.b(a1,4)於是就無限遞迴了,不重名就不會出現了。這是需要注意的一個點

有了上面的知識你能不能嘗試把property這個類寫出來呢?只要你懂了上面說的內容,這個應該不是什麼難事。

裡面加的print都是為了看清楚這個過程

我們現在已經把property給剖析得很深了,不知道你們是否還記得,上面幫助文件裡還有一個修飾符的例子,

其實就相當於

只不過也許你們發現了用修飾符修飾的寫法方法名一樣,引數不一樣,我們來試一下

格式比較固定,唯一可以變的就是@x.setter和@x.deleter的位置,修飾._x的方法名字必須一樣,還有修飾符前面也必須一樣。到這裡描述符就介紹完了。下面介紹一下定製容器

定製容器

什麼叫容器呢?前面講過的列表,元組,字典,集合,字串都是容器型別。而其中列表,元組,字串我們稱為序列,它們是有順序的,可以通過索引值訪問。而字典集合是雜湊的

,是沒有順序的,不叫做序列。容器型別就是儲存資料的嘛。首先談一下協議

也就說是一種規定而已,但不是絕對的,不是嚴格的。就比如說我們有frozenset和set,set是可以改變內容的,而frozenset不能,根據自己的需要來設定。

我們先來體會一下

元組型別是沒有__setitem__,__delitem__這些魔法方法的,這是python自己寫好的元組型別,如果你希望元組有這些魔法方法,你可以自己去定製一個元組。下面我們演示一個如何

定製一個不可以被改變的列表,按照這種思想,你完全可以去定製可以被改變的元組,你只需要新增__setitem__,__delitem__魔法方法就好。

如果你不記得fromkeys下面會幫助你理解

結果是ok的對吧。還有最後的幾個簡單的魔法方法在下面,就不演示了,因為比較簡單,前面也已經演示過很多的魔法方法了。

一個小建議

其實作用和C語言裡巨集定義一樣,有了BaseAias=BaseClass,以後如果BaseClass的名字換了,我們只需要改變一個地方就夠了。本講程式碼都不長,請自己打吧。