深入理解python元類
相對python的簡單來說,元類顯得有些複雜了,但是理解元類會讓你的python程式碼威力大大增加,理解元類需要了解一些python背後的東西,相信通過本文的敘述你能很快理解運用這一利器。
類的本質
首先我們思考下,python中的類是什麼,如下程式碼:
class ClassObject(): class_a = 1 def __init__(self): self.object_a = 2 def func(self): pass if __name__ == '__main__': print(ClassObject, id(ClassObject)) a = ClassObject() print(a, id(a)) print(ClassObject.class_a, a.class_a, a.object_a)
輸出如下:
(<class __main__.ClassObject at 0x000000000326C948>, 52873544L)
(<__main__.ClassObject instance at 0x0000000003482108>, 55058696L)
(1, 1, 2)
id用來輸出對應物件地址,可以看到這裡類其實也是一種物件,類例項由這個物件來建立,我們把這種結構畫成圖如下:
在匯入類時,記憶體中建立名字為ClassObject的物件,這個物件記憶體中的結構包括類屬性class_a和方法func,當我們使用a=ClassObject()時,python在名字空間中找到這個ClassObject物件,然後使用這個物件類建立類例項a,其中a物件記憶體中結構包括物件屬性object_a並指向上一級空間。
當使用print列印對應屬性時,在各自的記憶體空間中找到屬性並輸出值,對於物件a來說自身記憶體結構中找不到class_a,因此到上一級ClassObject空間中去找class_a。
總之,我們要明白,類本身在記憶體中就是一種物件結構。
元類的定義
既然,類是一種物件,那麼是誰來建立類這種物件呢。
自然是我們的元類(metaclass),所謂元就是根本、本源的意思(比如我們常說的元始天尊,哈哈)
元類需要繼承type,一個典型的元類定義如下:
class MetaClass(type): def __new__(cls, *args, **kwargs): return type('MetaClassNew', (), {"id":1}) if __name__ == '__main__': print(MetaClass) print(MetaClass()) b = MetaClass()() print(b, b.id)
輸出如下:
<class '__main__.MetaClass'>
<class '__main__.MetaClassNew'>
(<__main__.MetaClassNew object at 0x00000000032EF470>, 1)
第一行輸出表明就像之前說的,元類MetaClass也是一種物件,
第二行例項化MetaClass()建立的是類MetaClassNew,
b = MetaClass()()表示先例項化出類MetaClassNew,然後例項化類MetaClassNew得到MetaClassNew的物件。
type是一切元類的基類,通過它來完成類的產生,type('MetaClassNew', (), {"id":1})分別傳入類名、基類元組和類屬性字典即可完成類的動態生產。
因此可以看到,元類就是類的類,你要是願意,可以定義元類的元類的元類...,當然一般來說這沒有意義,但是卻說明了類和元類的關係。
元類的應用
那麼我們能用元類來做什麼呢?其實元類對應c++/java中的模板,這些語言中也有模板超程式設計的概念,所謂超程式設計,顧名思義就是修改根源,即對產生類的過程進行定製,在這些編譯語言中運用模板來做編譯時期的型別推斷進而實現泛型和一些編譯期的計算。
在python中類似,利用元類可以修改類的產生,對類的屬性進行定製,在類產生時進行一些操作,比如可以對類的類屬性進行校驗,動態註冊相關類等。總之就是攔截類的建立,做一些自定義Hook操作。
下面來看兩個常見的應用
1.類引數的校驗
實際應用中,對於類來說,如果引數錯誤,往往要我們在程式執行時才會動態判斷檢測,但是對於類本身的一些屬性在類匯入定義完成時就應該檢測出來。
比如,我們定義一個多邊形類,有一個固定屬性表示多邊形邊數,如果定義類時指定邊數<3,那顯然定義就是不合法的。
如下,我們定義一個元類,在類建立先做校驗:
class ValidatePolygon(type):
def __new__(meta, name, bases, class_dict):
print(meta, name, bases, class_dict)
if bases!=(type(object),):
if class_dict['sides']<3:
raise ValueError('Polygon needs 3+ sides')
return type.__new__(meta, name, bases, class_dict)
下面是多邊形定義,在2.X中通過__metaclass__類指定元類負責hook類的建立過程,在如上的hook中校驗多邊形的邊數。
print('Before class')
class Line(object):
__metaclass__ = ValidatePolygon
print('Before sides')
sides = 1
print('After sides')
print('After class')
匯入類時輸出如下:
Before class Before sides After sides After class ...ValueError: Polygon needs 3+ sides
可以看到,在類匯入完成後,校驗當前多邊形類的屬性不滿足條件。
2.類的自注冊
實際應用中,經常我們需要序列化和反序列化類,序列化很簡單,類按照一定結構存起來就行,關鍵是反序列化——字串形式儲存的物件如何對映到已有的類的物件,從而反序列化出對應的類例項。
最簡單的方法,我們維護一個map,每當有一個新類的時候,在map中新增一條對映記錄。這樣可以解決問題,但是如果開發人員忘記新增對映記錄,那麼就會造成反序列化失敗的大問題。
如果能夠在定義新類的時候自動在map中新增記錄就可以解決這個問題,這正是元類可以做的,hook類的建立並在此註冊新類,演示如下:
register_map = {}
def register_class(cls):
print('---Register class:%s---' %cls.__name__)
register_map[cls.__name__] = cls
class Meta(type):
def __new__(cls, name, bases, class_dict):
cls = type.__new__(cls, name, bases, class_dict)
register_class(cls)
return cls
class SelfRegisterSerializer(object):
__metaclass__ = Meta
def __init__(self, *args):
self.args = args
def serialize(self):
return json.dumps({'class':self.__class__.__name__,
'args':self.args})
@staticmethod
def dserialize(data):
params = json.loads(data)
name = params['class']
target_class = register_map[name]
return target_class(*params['args'])
可以看到,匯入類完成後,register_class名字對映到對應的類物件。 這樣當反序列化時target_class = register_map[name],
通過查詢對應名字的類物件來建立類例項即可。這樣這個類序列化和反序列化就非常簡單了,如下:
class MyClass1(SelfRegisterSerializer):
pass
class MyClass2(SelfRegisterSerializer):
pass
if __name__ == '__main__':
a1 = MyClass1(1,2,3)
print('Before:', a1)
data = a1.serialize()
print('Serialize:', data)
c1 = SelfRegisterSerializer.dserialize(data)
print('Dserialize:', c1)
這裡正常定義繼承類即可,匯入類時會自動註冊類,程式結構非常簡單。
這個例項可以擴充套件到很多應用場景,如IOC類建立,外掛動態載入等待,總之就是hook類建立時幹自己的活。
演示程式碼下載連結