metaclass
metaclass的英文直譯過來就是元類,這既是一個概念也可以認為是Python當中的一個關(guān)鍵字,不管怎么理解,對它的內(nèi)核含義并沒有什么影響。我們可以不必糾結(jié),就認為它是類的類的意思即可。在這個用法當中,支持我們自己定義一個類,使得它是后面某一個類的元類。
之前使用type動態(tài)創(chuàng)建類的時候,我們傳入了類名,和父類的tuple以及屬性的dict。在metaclass用法當中,其實核心相差不大,只是表現(xiàn)形式有所區(qū)別。我們來看一個例子即可:
classAddInfo(type):
def__new__(cls,name,bases,attr):
attr['info']='addbymetaclass'
returnsuper().__new__(cls,name,bases,attr)
classTest(metaclass=AddInfo):
pass
在這個例子當中,我們首先創(chuàng)建了一個類叫做AddInfo,這是我們定義的一個元類。由于我們希望通過它來實現(xiàn)元類的功能,所以我們需要它繼承type類。我們在之前的文章當中說過,在Python面向?qū)ο螽斨?,所有的類的根本來源就是type。也就是說Python當中的每一個類都是type的實例。
我們在這個類當中重載了__new__方法,我們在__new__方法當中傳入了四個參數(shù)。眼尖一點的小伙伴一定已經(jīng)看出來了,這個函數(shù)的四個參數(shù),正是我們調(diào)用type創(chuàng)建類的時候傳入的參數(shù)。其實我們調(diào)用type的方法來創(chuàng)建類的時候,就是調(diào)用的__new__這個函數(shù)完成的,這兩種寫法對應的邏輯是完全一樣的。
我們之后又創(chuàng)建了一個新的類叫做Test,這個當中沒有任何邏輯,直接pass。但是我們在創(chuàng)建類的時候指定了一個參數(shù)metaclass=AddInfo,這里這個參數(shù)其實就是指定的這個類的元類,也就是指定這個類的創(chuàng)建邏輯。雖然我們用代碼寫了類的定義,但是在實際執(zhí)行的時候,這個類是以metaclass為元類創(chuàng)建的。
根據(jù)上面的邏輯,我們可以知道,Test類在創(chuàng)建的時候就被賦予了類屬性info。我們可以驗證一下:
拓展類功能
上面這段就是元類的基本用法了,其實本質(zhì)上和我們之前介紹的type的動態(tài)類創(chuàng)建是一樣的,只不過展現(xiàn)的形式不同。那么我們就有一個問題要問了,我們使用元類究竟能夠做什么呢?
這里有一個經(jīng)典的例子,我們都知道Python原生的list是沒有'add'這個方法的。假設(shè)我們習慣了Java當中l(wèi)ist的使用,習慣用add來為它添加元素。我們希望創(chuàng)建一個新的類,在這個新的類當中,我們可以通過add來添加函數(shù)。通過元類可以很方便地使用這一點。
classListMeta(type):
def__new__(cls,name,bases,attrs):
#在類屬性當中添加了add函數(shù)
#通過匿名函數(shù)映射到append函數(shù)上
attrs['add']=lambdaself,value:self.append(value)
returnsuper().__new__(cls,name,bases,attrs)
classMyList(list,metaclass=ListMeta):
pass
我們首先是定義了一個叫做ListMeta的元類,在這個元類當中我們給類添加了一個屬性叫做add。它只是包裝了一下而已,底層是通過append方法實現(xiàn)的。我們來實驗一下:
從結(jié)果來看也沒什么問題,我們成功通過調(diào)用add方法往list當中插入了元素。這里藏著一個小細節(jié),我們在ListMeta當中為attrs添加了一個名叫'add'的屬性。這個屬性是添加給類的,而不是類初始化出來的實例的。所以如果我們print出MyList這個類當中的所有屬性,也能看到add的存在。
如果我們直接去通過MyList去訪問add方法的話會引起報錯,因為我們實現(xiàn)add這個方法邏輯的匿名函數(shù)限制了需要傳入兩個參數(shù)。第一個參數(shù)是實例的對象self,第二個參數(shù)才是添加的元素value。如果我們通過MyList的類屬性去訪問它的話會觸發(fā)一個錯誤,因為缺少了一個參數(shù)。因為類當中的屬性實例也是可以調(diào)用的,并且Python會在參數(shù)前面自動添加self這個參數(shù),就剛好滿足了要求。
搞明白了這些我們只是解決了可能性問題,我們明白了元類可以實現(xiàn)這樣的操作,但沒有解決我們?yōu)槭裁幢仨氁褂迷惸?就拿剛才的例子來說,我們完全可以繼承l(wèi)ist這個類,然后在其中再開發(fā)我們想要的方法,為什么一定要使用元類呢?
就剛才這個場景來說,的確,我們是找不出任何理由的。完全沒有理由不使用繼承,而非要用元類。但是在有些場景和有些問題當中,我們必須要使用元類不可。就是涉及類屬性變更和類創(chuàng)建的時候,我們來看下面這個例子。
控制實例的創(chuàng)建
還記得我們上篇文章介紹的工廠設(shè)計模式的例子嗎?就是我們可以通過參數(shù)來得到不同類的實例。
我們創(chuàng)建了三種游戲的類和一個工廠類,我們重載了工廠類的__new__函數(shù)。使得我們可以根據(jù)實例化時傳入的參數(shù)返回不同類型的實例。
classLast_of_us:
defplay(self):
print('theLastOfUsisreallyfunny')
classUncharted:
defplay(self):
print('theUnchartedisreallyfunny')
classPSGame:
defplay(self):
print('PShasmanygames')
classGameFactory:
games={'last_of_us':Last_of_us,'uncharted':Uncharted}
def__new__(cls,name):
ifnameincls.games:
returncls.games[name]()
else:
returnPSGame()
uncharted=GameFactory('uncharted')
last_of_us=GameFactory('last_of_us')
假設(shè)這個需求完成得很好順利上線了,但是運行了一段時間之后我們發(fā)現(xiàn)下游有的時候為了偷懶會不通過工廠類來創(chuàng)建實例,而是直接對需要的類做實例化。原本這沒有問題,但是現(xiàn)在產(chǎn)品想要在工廠類當中加上一些埋點,統(tǒng)計出訪問我們工廠的訪問量。所以我們需要限制這些游戲類不能直接實例化,必須要通過工廠返回實例。
那么這個功能我們怎么實現(xiàn)呢?
我們分析一下問題就會發(fā)現(xiàn),這一次不是需要我們在創(chuàng)建實例的時候做動態(tài)的添加,而是直接限制一些類不允許直接調(diào)用進行創(chuàng)建。限制的方法比較常用的一種就是拋出異常,所以我們希望可以給這些類加上一個邏輯,實例化類的時候傳入一個參數(shù),表明是否是通過工廠類進行的,如果不是,則拋出異常。
這里,我們需要用到另外一個默認函數(shù),叫做__call__,它是允許將類實例當做函數(shù)調(diào)用。我們通過類名來實例化,其實也是一個調(diào)用邏輯。這個__call__的邏輯并不難寫,我們隨手就來:
def__call__(self,*args,**kwargs):
iflen(args)==0orargs[0]!='factory':
raiseTypeError("Can'tinstantiatedirectly")
但問題是這個__call__函數(shù)并不能直接加在類當中,因為它的應用范圍是實例,而不是類。而我們希望的是在創(chuàng)建實例的時候進行限制,而不是對調(diào)用實例的時候進行限制,所以這段邏輯只能通過元類實現(xiàn)。
我們直接創(chuàng)建類的時候就會觸發(fā)異常,因為不是通過工廠創(chuàng)建的。我們這里判斷是否是工廠創(chuàng)建的邏輯簡化掉了,只是通過一個簡單的字符串來進行的判斷,實際上會用一些更加復雜的邏輯,這不是本文的重點,我們了解即可。
整體運行的邏輯和我們設(shè)想的一樣,說明這樣實現(xiàn)是正確的。
總結(jié)
我們?nèi)粘i_發(fā)當中用到元類的情況非常罕見,一般都是在一些高端開發(fā)的場景當中。比如說開發(fā)一些框架或者是中間件,為了方便下游的使用,需要創(chuàng)建一些關(guān)于類屬性的動態(tài)邏輯,才會用到元類。對于普通開發(fā)者而言,如果你無法理解元類的含義以及應用,也沒有關(guān)系,使用頻率非常低。
另外,元類的概念和動態(tài)類、動態(tài)語言的概念有關(guān),Python語言的動態(tài)特性很多正是通過這一點體現(xiàn)的。所以隨著我們對于Python動態(tài)特性理解的加深,理解元類也會變得越來越容易,同樣也會理解越來越深刻。如果我們把Python的元類和裝飾器做一個類比的話,會發(fā)現(xiàn)兩者的核心邏輯是很類似的。本質(zhì)上都是在原有的邏輯之外封裝新的邏輯,只不過裝飾器針對的是一段邏輯,而元類針對的是類的屬性和創(chuàng)建過程。
以上內(nèi)容為大家介紹了Python之metaclass的原理和用法,希望對大家有所幫助,如果想要了解更多Python相關(guān)知識,請關(guān)注IT培訓機構(gòu):千鋒教育。