標籤:

元類是什麼以及如何使用

99%的人不需要知道metaclass。需要知道metaclass的人往往已經知道怎麼使用它。


在Python中,一切都是對象,包括字元串、整數、函數和類,它們都由各自的元類創建。通過內建的type,可以查看創建該對象的元類:

def foo():n passnclass Bar:n passnnprint(type(1))nprint(type("hello"))nprint(type(foo))nprint(type(Bar))nnprint("n")nprint(type(type(1)))nprint(type(type("hello")))nprint(type(type(foo)))nn---輸出---n<class int> #1是元類int的一個對象n<class str> #"hello"是元類str的一個對象n<class function> #foo是元類function的一個對象n<class type> #Bar(及任何class)都是元類type的一個對象nnn<class type> #元類int是元類type的一個對象n<class type> #元類str是元類type的一個對象n<class type> #元類function是元類type的一個對象n

由類定義本身也是一個對象,所以你可以這樣操作一個類對象(類對象,而不是類的對象):

def foo(self, x):n self.value = xnnprint(id(Bar))nsetattr(Bar, value, 3)nprint(Bar.value)nnsetattr(Bar, foo, foo)nbar = Bar()nbar.foo(5)nprint(bar.value)n

為什麼type被稱之為元類?因為它可以用來創建任何類:

#---示例1---nclass Integer(int):nname = my integer classnndef inc(self, num):n return num + 1nn#---示例2---nInteger = type("Integer", (int, ), { #注意當tuple中只有一個元素時,必須加上一個逗號nname: "my integer class",ninc : lambda self, num: num + 1n})n

這兩段代碼是等價的,結果都是定義了一個名為Integer的自定義類。

使用type方法來創建類雖然可行,但代碼可讀性會差一點,所以python引入了__metaclass__:

models = {}nnclass ModelMetaclass(type):n def __new__(meta, name, bases, attrs):n models[name] = cls = type.__new__(meta, name, bases, attrs)n return clsnnclass Model(object, metaclass = ModelMetaclass):#python 3語法n # python 2語法n #__metaclass__ = ModelMetaclassn passnna = Model()nprint(models)n#---輸出---n{Model: __main__.Model}n

上面這個例子已經幾乎揭示出metaclass的實際作用。語法就介紹到這裡,我們來看一個實用的例子。

在客戶端與伺服器通訊的過程中,我們需要定義一些協議,比如使用JSON:

{n "id": "xxxx-xxxx-xxxx-xx",n "timestamp": 2017-10-1 00:00:00,n protocol: initi_communication,n from: 8.8.8.8,n to: 4.4.4.4,n params: [1, 2, 3]n}n

假設我們有很多協議要定義,而且這些協議都有通用的欄位,比如:

{n"id": "xxxx-xxxx-xxxx-xx",n"timestamp": "2017-10-1 00:00:00",n"protocol": initi_communicationn}n

以python的角度來看,我們更希望提供為每個協議生成一個類,並為這個類提供序列化到JSON以及從JSON反序列到類對象的方法,以及對一些通用欄位的初始化可以使用基類來處理。

這樣做的好處是,當你從網路上收到一個協議時,與純粹的JSON對象相比,你可以在序列化時進行語義合法性檢查,可以用isinstance進行類型判斷。在引用某個欄位的值時,使用model.from也要比model["from"]更容易輸入,而且在某些時候還能用上自動完成以減少輸入錯誤。

所以,我們的通訊協議類可以這樣來定義:

import timenimport uuidnimport jsonnclass Proto(object):n def __init__(self, protocol, id = None, timestamp = None):n self.protocol = protocoln self.id= id or str(uuid.uuid4())n self.timestamp = timestamp or int(time.time())nn def __repr__(self):n return json.dumps(self.__dict__, indent = 2)nnclass InitProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("InitProtocol")nnclass CloseProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("CloseProtocol")nna = InitProtocol()nb = CloseProtocol()nnprint(a,"n", b)nn#--輸出--n{n"id": "dab02550-137e-41b2-8e92-684a368c8ebf",n"timestamp": 1511348727.562287,n"protocol": "InitProtocol"n}n{n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}n

現在,要生成一個協議對象確實簡單多了,而且也方便調試,因為我們改寫了基類的__repr__方法。

但是如果我們從網路上收到這樣的數據:

{n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}n

並且想把它轉回為一個CloseProtocol的對象呢?我們可以為每個協議對象添加一個from_dict的方法:

response = {n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}nnclass CloseProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("CloseProtocol")nn @staticmethodn def from_dict(d):n o = CloseProtocol()n o.__dict__.update(d)nn return onnc = CloseProtocol.from_dict(response)nprint(c)n#--輸出--n{n"protocol": "CloseProtocol",n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366n}n

但這個方法其實並沒有那麼完美,因為你的代碼中必然有一處是這樣寫的:

response = json.loads(...)nnobj = Nonenif response["protocol"] == "CloseProtocol":nobj = CloseProtocol.from_dict(response)nelif response["protocol"] == "InitProtocol":nobj = InitProtocol.from_dict(response)nelif ...n

這裡,我們需要為每一個子類定義一個from_dict方法。既然每個子類都有from_dict方法,顯然我們應該將其提升到基類中去。另外,上面的判斷也應該放到基類里,因為其它人不需要有如何創建這些對象的知識。現在,我們的代碼可以改為:

import datetimenimport uuidnimport jsonnclass Proto(object):n def __init__(self, protocol, id = None, timestamp = None):n self.protocol = protocolnDomain Name Registration with Privacy & Cheap Price = id or str(uuid.uuid4())n self.timestamp = timestamp or datetime.datetime.now().timestamp()nn def __repr__(self):n return json.dumps(self.__dict__, indent = 2)nn @staticmethodn def from_dict(d):n o = Nonen if d["protocol"] == "CloseProtocol":n o = CloseProtocol()n o.__dict__.update(d)n elif d["protocol"] == "InitProtocol":n o = InitProtocol()n o.__dict__.update(d)nn return onnclass InitProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("InitProtocol")nnclass CloseProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("CloseProtocol")nna_json = {n"id": "dab02550-137e-41b2-8e92-684a368c8ebf",n"timestamp": 1511348727.562287,n"protocol": "InitProtocol"n}nnb_json = {n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}nna = Proto.from_dict(a_json)nb = Proto.from_dict(b_json)nnprint(a,"n", b)nn#--輸出--n{n"protocol": "InitProtocol",n"id": "dab02550-137e-41b2-8e92-684a368c8ebf",n"timestamp": 1511348727.562287n}n{n"protocol": "CloseProtocol",n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366n}n

OK,現在你終於有了一個臃腫不堪的基類,而且更可怕的是,每定義一個新的子類,你都必須去修改基類的from_dict方法。除非一個人信奉某種暴力美學,這幾乎是無法忍受的。我們首先使用兩個技巧來解決這個問題:

一是使用inspect來自動獲取所有子類的名字,二是使用__class__方法來動態修改父類的類型:

import timenimport uuidnimport jsonnclass Proto(object):n def __init__(self, protocol = None, id = None, timestamp = None):n self.protocol = protocoln self.id = id or str(uuid.uuid4())n self.timestamp = timestamp or time.time()nn def __repr__(self):n return json.dumps(self.__dict__, indent = 2)nn @staticmethodn def from_dict(_dict):n o = Proto()n o.__dict__.update(_dict)n o.__class__ = registry[o.protocol]n return onnclass InitProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("InitProtocol")nnclass CloseProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("CloseProtocol")nndef register_class():n import sys, inspectn registry = {}n for name, obj in inspect.getmembers(sys.modules[__name__]):n if inspect.isclass(obj):n if name.endswith("Protocol"):n registry[name] = objnn return registrynnregistry = register_class()nna_json = {n"id": "dab02550-137e-41b2-8e92-684a368c8ebf",n"timestamp": 1511348727.562287,n"protocol": "InitProtocol"n}nnb_json = {n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}nna = Proto.from_dict(a_json)nb = Proto.from_dict(b_json)nnprint(a,"n", b)n

我們用了大量的篇幅來講如何從網路包恢復成一個對象。可這與metaclass有什麼關係?因為上面的代碼使用了inspect模塊,而且還使用了一個全局變數以及一些額外的代碼,這些仍然是我們不想看到的。

import datetimenimport uuidnimport jsonnnclass ProtoMeta(type):n _registry = {}n def __new__(meta, name, bases, attrs):n meta._registry[name] = cls = type.__new__(meta, name, bases, attrs)n return clsnnclass Proto(object, metaclass = ProtoMeta):n def __init__(self, protocol = None, id = None, timestamp = None):n self.protocol = protocoln self.id = id or str(uuid.uuid4())n self.timestamp = timestamp or int(time.time())nn def __repr__(self):n return json.dumps(self.__dict__, indent = 2)nn @staticmethodn def from_dict(_dict):n o = Proto()n o.__dict__.update(_dict)n o.__class__ = Proto._registry[o.protocol]n return onnclass InitProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("InitProtocol")nnclass CloseProtocol(Proto):n def __init__(self, id = None, timestamp = None):n super().__init__("CloseProtocol")nn# test codena_json = {n"id": "dab02550-137e-41b2-8e92-684a368c8ebf",n"timestamp": 1511348727.562287,n"protocol": "InitProtocol"n}nnb_json = {n"id": "f56d857c-bfc2-4646-9105-f9d73964e94a",n"timestamp": 1511348727.562366,n"protocol": "CloseProtocol"n}nna = Proto.from_dict(a_json)nb = Proto.from_dict(b_json)nnprint(a,"n", b)n

在這裡,我們使用了metaclass去hook住每一個子類對象的創建過程,並將類對象保存在ProtoMeta的類成員變數_registry中,然後在from_dict方法中使用這個_registry。至此,我們完成了通訊協議模塊的徹底封裝:

1. 定義一個新的子類只要從Proto繼承,大多數屬性和方法已經由基類定義並實現,比如__repr__和to_json

2. 只要某個通訊協議已經聲明了對應的類,那麼都可以通過基類Proto.from_dict方法自動恢復出來。

通過上面的例子,應該清楚地了解了元類的作用了。metaclass在python內建的庫中也有使用,比如ABCAbstract類(使用了ABCMeta)。在第三方庫中,比較有名的有Django的ORM。


推薦閱讀:

Python數據抓取(3) —抓取標題、時間及鏈接
不懂編程,如何才能學好python呢?
python 括弧檢測是否匹配?

TAG:Python |