當我們使用Python進行開發時,當中的資料都是以物件的形式存在,而物件會是類別的實例,若能控制每個被定義的類別,就有機會對它們進行驗證、調整,玩弄一些神奇的技法。若要概略區分,這其實是屬於meta程式設計的範疇。如果你對Python稍有深入的研究,可能會想到Python可藉由metaclass的定義與指定,來從事meta程式設計。

然而,在Python中,想利用meta程式設計完成任務,最好的方式就是不要使用metaclass,這聽來矛盾,其實這是個隱喻,就跟「在Python中想讀取檔案,最好的方式就是不要read」是類似的,因為檔案物件實作了迭代器協定,透過for in就能逐一取得檔案中的資料,同時可以降低讀取檔案時的複雜度。

使用metaclass之前

類似地,metaclass在實作與使用上有著不小的複雜度,往往還必須搭配裝飾器(decorator)、描述器(descriptor),而隨意實作的metaclass令程式增加意外性且難以除錯,因此我們應優先考量Python標準程式庫中現成、成熟的metaclass實作方案,看看它們是否能夠解決問題,而非一上來就直接實作metaclass。

例如,在屬性與方法的控制上,如果想實現固定存取(uniform access)原則,我們可以使用@property,靜態方法與類別方法也有相對應的@staticmethod、@classmethod裝飾器;若想定義抽象基礎類別及抽象方法,我們可以透過abc模組的ABC類別,以及@abstractmethod等進行。

基本上,@property等標註,實現了裝飾器與描述器的行為,至於ABC類別,其實是個指定metaclass為abc.ABCMeta的簡便類別,而建議繼承ABC的原因在於,可以讓開發者不用意識到metaclass的存在。而在多重繼承的情況下,若其他父類別也指定metaclass,建構與初始的流程可能會難以掌握,這時自行指定metaclass才會是必要的。

魔術方法與描述器

如果Python標準程式庫現成的方案不足以使用,下一步是考量可靠的第三方程式庫,如果還是不足以解決問題,想要自行實現屬性與方法的控制,我們此時可以先考量魔術方法。

例如,若想控制能指定給物件的屬性名稱,我們可以在定義類別時指定__slots__,列出類別實例可以指定的屬性;若想指明某些特性是抽象方法,我們可以在類別實例上指定__abstractmethods__,表明哪一些方法其實是抽象方法,如果類別的__abstractmethods__不為空,那麼就是個抽象類別,不可以直接實例化。

然而,只是指定父類別的__abstractmethods__,並沒有檢查子類別是否實作抽象方法的能力,這就是ABC類別與@abstractmethod存在之目的。其實,檢查子類別是否實現了抽象方法,只是metaclass用來驗證子類別的一種形式,有許多任務也都需要驗證子類別。

為了避免開發者以複雜的方式實現metaclass來驗證子類別,Python 3.6以後,類別可以實作__init_subclass__方法,若繼承該類別,直譯器在執行完類別定義後,就會呼叫__init_subclass__方法傳入子類別,這就使得驗證子類別這類任務在實作上,可以避免使用metaclass而輕鬆許多。

控制屬性的魔術方法,還有__getattribute__、__getattr__、__setattr__、__delattr__。在取得屬性時,最先執行的會是__getattribute__,常作為取得屬性時最前端的攔截器,而由於實例__dict__中存在屬性時就會傳回值,因此,__getattr__常作為搜尋屬性的過程中,都找不到屬性時的最後一道防線;由於__setattr__、__delattr__一定會先呼叫,所以,也就不存在__setattribute__、__delattribute__。

描述器會參與屬性存取過程,是與__getattr__需求正交(orthogonal)的方案,但與特殊方法不同之處在於:描述器可以取得描述器所在類別資訊,而且描述器本身是個物件,可重用在各個類別之中,本身被存取時會呼叫對應的協定,而非描述器所在類別之實例各屬性被存取時,採取對應的動作。

在Python 3.5以前,描述器只在被存取時才會呼叫對應的協定,若要在描述器被定義至類別時,就能進行驗證類別的動作,必須透過metaclass來實作;到了Python 3.6以後,描述器可以定義__set__name__方法,而這個方法會在類別定義完成後執行,傳入描述器所在之類別,這麼一來,就有機會對類別或名稱做些處理,可以避免過去一些必須實作metaclass的場合。

裝飾器與metaclass

meta程式設計的應用,並不僅在於控制屬性與方法,例如,Python 3.6以後的類別,可以實作__init_subclass__方法、描述器可以定義__set__name__方法,這無非就是在反映一件事實:不少任務需要在類別定義完成時就執行。

有個最接近類別定義完成時就執行的機制,那就是類別裝飾器,當裝飾器標註在類別,類別會作為裝飾器的引數傳入,這時候也就有機會對類別做驗證及調整。

因為每個類別只能指定一個metaclass,然而,類別裝飾器可以組合,也就是,在類別上可以標註多個裝飾器,這就使得在許多場合當中,類別裝飾器成了比metaclass更有彈性的選擇,例如,在《Effective Python》第二版的〈做法51〉,就推薦以類別裝飾器來實現可組合的擴充機制,而不是使用metaclass。

metaclass存在之目的,是希望類別定義完成時就執行一些任務,具體來說,就是希望能掌握類別的建立與初始化!嗯?類別不是寫死的程式碼定義嗎?不是!類別最後會成為一個物件,是type類別的實例,因此實作metaclass,就是希望在掌握type(...)建立、初始類別時的過程。

這也就是為什麼,定義metaclass時需要繼承type類別,實作__new__或__init__方法。這是因為__new__掌握了怎麼建立類別,__init__掌握了怎麼初始類別,如果某個類別在定義時指定了metaclass,那麼,就會使用該metaclass來建立、初始化類別。

建立、初始化類別前或後?

類別裝飾器(或者魔術方法、描述器等)終究是在類別建立、初始化之後,才會在類別進行調整,是否使用metaclass的最大考量也在於此:開發者想解決的任務,究竟是在類別建立、初始化之前就要完成呢?還是其實在類別建立、初始化之後,也可以實現呢?若是前者,才使用metaclass!

一個有趣的事實是,定義類別時metaclass可指定對象──並不只有type子類別,只要是可呼叫(callable)物件都可以指定,這是因為:建立metaclass實例時,就是呼叫__call__,其中才呼叫了__new__、__init__方法,然而,這是從鴨子定型的觀點來看罷了,如果使用靜態分析工具,像是mypy檢查時,可能就會發生"Invalid metaclass"之類的錯誤。

總之,在Python中,如果我們能用簡單方案實現的任務,就不要以複雜的方式來實現,從事meta程式設計時也是如此,雖然某些需求,看似用特殊方法、描述器或metaclass等都能實現,但是,還是要仔細考量彼此的分野,以免讓實現變得複雜了!

專欄作者


熱門新聞

Advertisement