Python進階——詳解元類,metaclass的原理和用法

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是Python專題第18篇文章,我們來繼續聊聊Python當中的元類。

在上上篇文章當中我們介紹了type元類的用法,在上一篇文章當中我們介紹了__new__函數與__init__函數的區別,以及它在一些設計模式當中的運用。這篇文章我們來看看metacalss與元類,以及__new__函數在元類當中的使用。

上一篇文章非常重要,是這一篇的基礎,如果錯過了上篇文章,推薦回顧一下:

Python面試常見問題,__init__是構造函數嗎?

metaclass

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的實例。

我們在這個類當中重載了__new__方法,我們在__new__方法當中傳入了四個參數。眼尖一點的小夥伴一定已經看出來了,這個函數的四個參數,正是我們調用type創建類的時候傳入的參數。其實我們調用type的方法來創建類的時候,就是調用的__new__這個函數完成的,這兩種寫法對應的邏輯是完全一樣的。

我們之後又創建了一個新的類叫做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 排版

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新