2.1.5 Python元類深刻理解(metaclass)

花姐毛毛腿發表於2019-01-21

點選跳轉Python筆記總目錄

本節目錄

一、建立類的執行流程

二、元類的認識

三、元類的示例


一、建立類的執行流程

類建立過程中所需要的資訊

一個python類在建立過程中,需要獲取兩種型別的資訊,即:動態元資訊,與靜態元資訊。所謂動態元資訊,是指那些隨著類的變化會改變的資訊。比如:類名稱,類基類,類屬性。而所謂靜態元資訊,是指與類的種類沒有關係的靜態資訊,這裡主要指的是類建立的方式和過程。我想,對於動態元資訊,大家是再熟悉不過了。而對於靜態元資訊,其概念顯得晦澀難懂。然而它其實有一個響亮的名字:元類。

元類

我們都知道,python 處處皆物件。因此,在python中,類(class)本身也是一個物件(object),而元類則是類的型別。在我的文章深入理解python之物件系統中,我也曾經梳理過,普通物件(PyXXX_Object),類物件(PyXXX_Type),基類物件(PyBaseObject_Type)以及元類(PyType_TYpe)之間的關係。

在這裡插入圖片描述

如果沒有指定,那麼所有類的型別(ob_type)都指向type物件(PyType_Type)。也就是說所有型別物件的預設元類就是這個type。

我們前面提到類的靜態元資訊,即為元類。也就是說,元類描述了類的建立方式和過程,那麼它是如何做到的呢?這裡我們閱讀如下python原始碼:

static PyObject *build_class(PyObject *methods, PyObject *bases, PyObject *name){ 
//methods:類屬性列表 //bases:基類元組 //name:類名稱 PyObject *metaclass = NULL;//元類 ... //從methods裡尋找使用者自定義的metaclass,如果找不到,則使用預設的metaclass ... result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods, NULL);

}PyObject_CallFunctionObjArgs(PyObject *callable, ...){
... tmp = PyObject_Call(callable, args, NULL);
... return tmp;

}PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw){
//func=metaclass //args=[methods,bases,name] ... call = func->
ob_type->
tp_call;
... result = (*call)(func, arg, kw);
...
}複製程式碼

build_class 函式是建立類物件過程中的一個核心函式,build_class函式傳入的引數是類的動態元資訊:屬性,類基類,類名。而build_class做的第一件事是,確定類的元類(metaclass)。在找到了元類以後,實際上python底層在這裡對元類物件執行了一個 呼叫(call)的動作,就像呼叫一個函式物件那樣。假設某個類物件的元類物件是metaclass。那麼在類建立過程中圍繞元類的核心操作如下:

class=metaclass(metaclass,methods,bases,name)複製程式碼

由此我們可以知道,所謂的類物件的靜態元資訊,也就是類物件建立的過程與方式,都封裝在了某個callable的metaclass物件的函式體內。

與之相對比的,建立某個普通物件的過程也就是對於一個類物件的呼叫過程:

obj=class(class,...);
複製程式碼

class 是metaclass的例項,所以呼叫metaclass得到class。而obj是class的例項,所以呼叫class得到obj。在這裡,python “ 處處皆物件”的設計哲學得以很好的體現。我們由此可以做個總結:在python中,類是通過 “呼叫(call)“的方式建立一個物件。

類建立過程的原理分析

本小節我們通過一個具體的例子去分析,類建立過程中的具體步驟。例項程式碼:

class meta(type):    '''元類'''    def __new__(metacls,name,bases,methods):        print metacls,name        return type.__new__(metacls,name,bases,methods)    def __init__(cls,name,bases,methods):        print cls,nameclass T(object):    '''類'''    __metaclass__=meta #指定元類    a=1    def b():        pass    def c():        pass複製程式碼

首先我們通過compile 函式和dis模組去獲取型別T定義過程對應的指令序列:

22 LOAD_CONST               2 ('T')25 LOAD_NAME                2 (object)28 BUILD_TUPLE              131 LOAD_CONST               3 (<
code object T at 0x1013eac30, file "", line 18>
)34 MAKE_FUNCTION 037 CALL_FUNCTION 040 BUILD_CLASS 41 STORE_NAME 3 (T)44 LOAD_CONST 4 (None)47 RETURN_VALUE複製程式碼
  • LOAD_CONST 是向當前直譯器執行棧內壓入類名T。

  • LOAD_NAME 和BUILD_TUPLE指令實際上完成了對於類基類元組(bases)的準備,BUILD_TUPLE 指令執行完成後,當前直譯器執行棧上已經存入了函式名和基類元組。

  • LOAD_CONST,MAKE_FUNCTION,CALL_FUNCTION 三條指令,實際上完成了類屬性字典的定義與收集。

  • LOAD_CONST 載入的是類中屬性定義語句所對應的PyCodeObject。

  • MAKE_FUNCTION 是利用類中屬性定義語句的PyCodeObject 創造一個PyFunctionObject

  • CALL_FUNCTION 則是使直譯器執行類中屬性定義語句的指令序列,從而完成類屬性的定義過程。

  • BUILD_CLASS 顯然是建立一個類的核心指令,其實際上就是呼叫了前文所述的build_class函式。

  • STORE_NAME 指令將經過BUILD_CLASS 指令之後建立的類物件,存入名字T所對應的空間,至此,整個類的建立過程結束。

接下來,我們重新回到整個類建立過程的核心build_class 函式。

直譯器在處理我們的例項程式碼過程中,顯然有:

//T_pro_dict={"__metaclass__"=meta,"a":1,"b":...
}//T_bases=[object]build_class(T_pro_dict,T_bases,"T");
複製程式碼

所以在PyObject_Call中有:

PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw)    { 
//func=meta //args=[T_pro_dict,T_bases,"T"] ... call = func->
ob_type->
tp_call;
... result = (*call)(func, arg, kw);
...
}複製程式碼

這裡我們思考,meta->
ob_type->
tp_call是什麼?meta是我們定義的元類物件,作為一個型別物件本身,其ob_type 是指向type物件的,因此meta->
ob_type->
tp_call 指向的是type物件的tp_call 成員,也就是:PyType_type->
tp_call。繼續追蹤PyType_type->
tp_call的原始碼:

static PyObject *type_call(PyTypeObject *type, PyObject *args, PyObject *kwds){ 
//type = meta //args=[T_pro_dict,T_bases,"T"] PyObject *obj;
... obj = type->
tp_new(type, args, kwds);
... if(type->
tp_init &
&
type != &
PyType_Type) type->
tp_init(obj, args, kwds); ... return obj;

}複製程式碼

在pyType_type->
tp_call 所對應的函式裡,首先呼叫了metaclass 的new函式,又由於當前元類不是預設的type型別,因此也會執行metaclass的init函式。落實到在我們的示例程式碼中,顯然有如下邏輯得以執行:

class_obj=meta.__new__(meta,"T",T_bases,T_pro_dict)meta.__init__(class_obj,"T",T_bases,T_pro_dict)複製程式碼

從例項程式碼的執行結果也可以得到印證:

<
class '__main__.meta'>
T<
class '__main__.T'>
T複製程式碼

通過名字我們不難猜測,這裡的_new_ 相當於python 類物件的建構函式,實際負責了類物件記憶體的申請,動態元資訊的填充等工作。而_init_ 則是一個可選的初始化函式,由元類的定製者設計其中的內容。在我們的例項中,meta._new函式呼叫了type._new函式完成了類物件的建立,type.new 在python原始碼中對應的的是type_new函式。

static PyObject *type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds){ 
PyObject *name, *bases, *dict;
... // 獲取目標類的動態元資訊:類名,基類元組,屬性字典 PyArg_ParseTupleAndKeywords(args, kwds, "SO!O!:type", kwlist, &
name, &
PyTuple_Type, &
bases, &
PyDict_Type, &
dict);
... // 類物件的記憶體申請 type = (PyTypeObject *)metatype->
tp_alloc(metatype, nslots);
/*類物件的動態元資訊填充*/ //類名 type->
tp_name = PyString_AS_STRING(name);
... //基類 type->
tp_bases = bases;
... //屬性 type->
tp_dict = dict = PyDict_Copy(dict);
... // 型別的其他資訊的初始化 PyType_Ready(type);
... return (PyObject *)type;

}複製程式碼

在type_new函式的中,我們可以清楚的看到類物件記憶體申請,動態元資訊填充的具體實現。至此,一個元類建立一個類的主幹過程,就梳理完畢了。

二、元類的認識

什麼是元類呢?在Python3中繼承type的就是元類

三、元類的示例

方式一:

class MyType(type):    '''繼承type的就是元類'''    def __init__(self,*args,**kwargs):        print("MyType建立的物件",self)   #Foo        super(MyType,self).__init__(*args,**kwargs)    def __call__(self, *args, **kwargs):        obj = super(MyType,self).__call__(*args,**kwargs)        print("類建立物件",self,obj)   #Fooclass Foo(object,metaclass=MyType): #  物件加括號會去執行__call__方法,__call__方法裡面繼承了type的__call__方法type的__call__方法裡面會先執行__new__方法,再去執行__init__方法。                                      所以,Foo就是用type建立出來的    user = "haiyan"    age = 18obj = Foo()複製程式碼

方式二:

class MyType(type):    def __init__(self, *args, **kwargs):        print("ssss")        super(MyType, self).__init__(*args, **kwargs)    def __call__(cls, *args, **kwargs):        v = dir(cls)        obj = super(MyType, cls).__call__(*args, **kwargs)        return obj#物件加括號就會去執行__call__方法class Foo(MyType('Zcc', (object,), {
})): #MyType('Zcc', (object,), {
})相當於class Zcc(object):pass,也就是建立了一個Zcc的類
user = 'haiyan' age = 18obj = Foo()複製程式碼

方式三:

class MyType(type):    def __init__(self, *args, **kwargs):        print("ssss")        super(MyType, self).__init__(*args, **kwargs)    def __call__(cls, *args, **kwargs):        v = dir(cls)        obj = super(MyType, cls).__call__(*args, **kwargs)        return obj#物件加括號就會去執行__call__方法def with_metaclass(arg,base):    print("類物件",MyType('Zcc', (base,), {
})) return arg('Zcc', (base,), {
}) #返回一個類物件 <
class '__main__.Zcc'>
class Foo(with_metaclass(MyType,object)): #MyType('Zcc', (object,), {
})相當於class Zcc(object):pass,也就是建立了一個Zcc的類
user = 'haiyan' age = 18obj = Foo()複製程式碼
class ASD(type):    passqqq = ASD("qwe", (object,), {
}) #用ASD這個元類建立了一個(qwe,並且繼承object類的)類# class ASD(qwe):# passobj = qqq()# 能建立類的是元類# 能建立物件的是類print(obj) #<
__main__.qwe object at 0x00000000024FFBA8>
print(obj.__class__) #<
class '__main__.qwe'>
print(obj.__class__.__class__) #<
class '__main__.ASD'>
print(obj.__class__.__class__.__class__) #<
class 'type'>
print(obj.__class__.__class__.__class__.__class__) #<
class 'type'>
複製程式碼

來源:https://juejin.im/post/5c4589f0e51d4507fb1d7962

相關文章