本文始發於我的公衆號:TechFlow,原創不易,求個關注web
今天是Python專題第18篇文章,咱們來繼續聊聊Python當中的元類。面試
在上上篇文章當中咱們介紹了type元類的用法,在上一篇文章當中咱們介紹了__new__函數與__init__函數的區別,以及它在一些設計模式當中的運用。這篇文章咱們來看看metacalss與元類,以及__new__函數在元類當中的使用。設計模式
上一篇文章很是重要,是這一篇的基礎,若是錯過了上篇文章,推薦回顧一下:app
Python面試常見問題,__init__是構造函數嗎?框架
metaclass的英文直譯過來就是元類,這既是一個概念也能夠認爲是Python當中的一個關鍵字,無論怎麼理解,對它的內核含義並無什麼影響。咱們能夠沒必要糾結,就認爲它是類的類的意思便可。在這個用法當中,支持咱們本身定義一個類,使得它是後面某一個類的元類。編輯器
以前使用type動態建立類的時候,咱們傳入了類名,和父類的tuple以及屬性的dict。在metaclass用法當中,其實核心相差不大,只是表現形式有所區別。咱們來看一個例子便可:函數
class AddInfo(type):
def __new__(cls, name, bases, attr): attr['info'] = 'add by metaclass' return super().__new__(cls, name, bases, attr) class Test(metaclass=AddInfo): pass 複製代碼
在這個例子當中,咱們首先建立了一個類叫作AddInfo,這是咱們定義的一個元類。因爲咱們但願經過它來實現元類的功能,因此咱們須要它繼承type類。咱們在以前的文章當中說過,在Python面向對象當中,全部的類的根原本源就是type。也就是說Python當中的每個類都是type的實例。url
咱們在這個類當中重載了__new__方法,咱們在__new__方法當中傳入了四個參數。眼尖一點的小夥伴必定已經看出來了,這個函數的四個參數,正是咱們調用type建立類的時候傳入的參數。其實咱們調用type的方法來建立類的時候,就是調用的__new__這個函數完成的,這兩種寫法對應的邏輯是徹底同樣的。spa
咱們以後又建立了一個新的類叫作Test,這個當中沒有任何邏輯,直接pass。可是咱們在建立類的時候指定了一個參數metaclass=AddInfo,這裏這個參數其實就是指定的這個類的元類,也就是指定這個類的建立邏輯。雖然咱們用代碼寫了類的定義,可是在實際執行的時候,這個類是以metaclass爲元類建立的。設計
根據上面的邏輯,咱們能夠知道,Test類在建立的時候就被賦予了類屬性info。咱們能夠驗證一下:
上面這段就是元類的基本用法了,其實本質上和咱們以前介紹的type的動態類建立是同樣的,只不過展示的形式不一樣。那麼咱們就有一個問題要問了,咱們使用元類究竟可以作什麼呢?
這裏有一個經典的例子,咱們都知道Python原生的list是沒有'add'這個方法的。假設咱們習慣了Java當中list的使用,習慣用add來爲它添加元素。咱們但願建立一個新的類,在這個新的類當中,咱們能夠經過add來添加函數。經過元類能夠很方便地使用這一點。
class ListMeta(type):
def __new__(cls, name, bases, attrs): # 在類屬性當中添加了add函數 # 經過匿名函數映射到append函數上 attrs['add'] = lambda self, value: self.append(value) return super().__new__(cls, name, bases, attrs) class MyList(list, metaclass=ListMeta): pass 複製代碼
咱們首先是定義了一個叫作ListMeta的元類,在這個元類當中咱們給類添加了一個屬性叫作add。它只是包裝了一下而已,底層是經過append方法實現的。咱們來實驗一下:
從結果來看也沒什麼問題,咱們成功經過調用add方法往list當中插入了元素。這裏藏着一個小細節,咱們在ListMeta當中爲attrs添加了一個名叫'add'的屬性。這個屬性是添加給類的,而不是類初始化出來的實例的。因此若是咱們print出MyList這個類當中的全部屬性,也能看到add的存在。
若是咱們直接去經過MyList去訪問add方法的話會引發報錯,由於咱們實現add這個方法邏輯的匿名函數限制了須要傳入兩個參數。第一個參數是實例的對象self,第二個參數纔是添加的元素value。若是咱們經過MyList的類屬性去訪問它的話會觸發一個錯誤,由於缺乏了一個參數。由於類當中的屬性實例也是能夠調用的,而且Python會在參數前面自動添加self這個參數,就恰好知足了要求。
搞明白了這些咱們只是解決了可能性問題,咱們明白了元類能夠實現這樣的操做,但沒有解決咱們爲何必需要使用元類呢?就拿剛纔的例子來講,咱們徹底能夠繼承list這個類,而後在其中再開發咱們想要的方法,爲何必定要使用元類呢?
就剛纔這個場景來講,的確,咱們是找不出任何理由的。徹底沒有理由不使用繼承,而非要用元類。可是在有些場景和有些問題當中,咱們必需要使用元類不可。就是涉及類屬性變動和類建立的時候,咱們來看下面這個例子。
還記得咱們上篇文章介紹的工廠設計模式的例子嗎?就是咱們能夠經過參數來獲得不一樣類的實例。
咱們建立了三種遊戲的類和一個工廠類,咱們重載了工廠類的__new__函數。使得咱們能夠根據實例化時傳入的參數返回不一樣類型的實例。
class Last_of_us:
def play(self): print('the Last Of Us is really funny') class Uncharted: def play(self): print('the Uncharted is really funny') class PSGame: def play(self): print('PS has many games') class GameFactory: games = {'last_of_us': Last_of_us, 'uncharted': Uncharted} def __new__(cls, name): if name in cls.games: return cls.games[name]() else: return PSGame() uncharted = GameFactory('uncharted') last_of_us = GameFactory('last_of_us') 複製代碼
假設這個需求完成得很好順利上線了,可是運行了一段時間以後咱們發現下游有的時候爲了偷懶會不經過工廠類來建立實例,而是直接對須要的類作實例化。本來這沒有問題,可是如今產品想要在工廠類當中加上一些埋點,統計出訪問咱們工廠的訪問量。因此咱們須要限制這些遊戲類不能直接實例化,必需要經過工廠返回實例。
那麼這個功能咱們怎麼實現呢?
咱們分析一下問題就會發現,這一次不是須要咱們在建立實例的時候作動態的添加,而是直接限制一些類不容許直接調用進行建立。限制的方法比較經常使用的一種就是拋出異常,因此咱們但願能夠給這些類加上一個邏輯,實例化類的時候傳入一個參數,代表是不是經過工廠類進行的,若是不是,則拋出異常。
這裏,咱們須要用到另一個默認函數,叫作__call__,它是容許將類實例當作函數調用。咱們經過類名來實例化,其實也是一個調用邏輯。這個__call__的邏輯並不難寫,咱們隨手就來:
def __call__(self, *args, **kwargs):
if len(args) == 0 or args[0] != 'factory': raise TypeError("Can't instantiate directly") 複製代碼
但問題是這個__call__函數並不能直接加在類當中,由於它的應用範圍是實例,而不是類。而咱們但願的是在建立實例的時候進行限制,而不是對調用實例的時候進行限制,因此這段邏輯只能經過元類實現。
咱們直接建立類的時候就會觸發異常,由於不是經過工廠建立的。咱們這裏判斷是不是工廠建立的邏輯簡化掉了,只是經過一個簡單的字符串來進行的判斷,實際上會用一些更加複雜的邏輯,這不是本文的重點,咱們瞭解便可。
總體運行的邏輯和咱們設想的同樣,說明這樣實現是正確的。
咱們平常開發當中用到元類的狀況很是罕見,通常都是在一些高端開發的場景當中。好比說開發一些框架或者是中間件,爲了方便下游的使用,須要建立一些關於類屬性的動態邏輯,纔會用到元類。對於普通開發者而言,若是你沒法理解元類的含義以及應用,也沒有關係,使用頻率很是低。
另外,元類的概念和動態類、動態語言的概念有關,Python語言的動態特性不少正是經過這一點體現的。因此隨着咱們對於Python動態特性理解的加深,理解元類也會變得愈來愈容易,一樣也會理解愈來愈深入。若是咱們把Python的元類和裝飾器作一個類比的話,會發現二者的核心邏輯是很相似的。本質上都是在原有的邏輯以外封裝新的邏輯,只不過裝飾器針對的是一段邏輯,而元類針對的是類的屬性和建立過程。
仔細思考,我相信必定會有靈光乍現的感受。
今天的文章就到這裏,若是喜歡本文,能夠的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。
本文使用 mdnice 排版