1. 程式人生 > 其它 >元類metaclass

元類metaclass

目錄

元類metaclass

一 什麼是元類

#### python中一切皆是物件。以如下程式碼為例分析:

# 元類=》OldboyTeacher類=》obj
class OldboyTeacher(object):
    school = 'oldboy'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say(self):
        print('%s says welcome to the oldboy to learn Python' % self.name)
#### 	所有的物件都是例項化或者說呼叫類而得到的(呼叫類的過程稱為類的例項化) ,比如物件obj是呼叫類OldboyTeacher得到的

obj = OldboyTeacher('egon', 18)  # 呼叫OldboyTeacher類=》物件obj
#                                  呼叫元類=》OldboyTeacher類

# print(type(obj))  #檢視物件obj的類是<class '__main__.OldboyTeacher'>
#### 如果一切皆為物件,那麼類OldboyTeacher本質上也是一個物件, 既然所有的物件都是呼叫類得到的,那麼OldboyTeacher必然也是呼叫一個類得到的,這個類稱之為元類。

print(type(OldboyTeacher))  # 結果為<class 'type'>,證明是呼叫了type這個元類而產生的OldboyTeacher,即預設的元類為type
# 結論:
	預設的元類是type,預設的情況下,我們使用class關鍵字定義的類都是由type產生的。

![例項化](https://gitee.com/chaochaofan/img/raw/master/001 例項化.png)

1 類定義被執行時的步驟

1) 解析 MRO 條目;	  
	# 如果在類定義中出現的基類不是 type 的例項,則使用 __mro_entries__ 方法對其進行搜尋,當找到結果時,它會以原始基類元組做引數進行呼叫。此方法必須返回類的元組以替代此基類被使用。元組可以為空,在此情況下原始基類將被忽略。

2) 確定適當的元類;		
	# 為一個類定義確定適當的元類是根據以下規則:
    2.1) 如果沒有基類且沒有顯式指定元類,則使用 type();
    2.2) 如果給出一個顯式元類而且 不是 type() 的例項,則其會被直接用作元類;
    2.3) 如果給出一個 type() 的例項作為顯式元類,或是定義了基類,則使用最近派生的元類。
	- 最近派生的元類會從顯式指定的元類(如果有)以及所有指定的基類的元類(即 type(cls))中選取。最近派生的元類應為 所有 這些候選元類的一個子型別。如果沒有一個候選元類符合該條件,則類定義將失敗並丟擲 TypeError。

3) 準備類名稱空間;		
	- 一旦確定了適當的元類,則將準備好類名稱空間。 如果元類具有 __prepare__ 屬性,它會以 namespace = metaclass.__prepare__(name, bases, **kwds) 的形式被呼叫(其中如果有任何額外的關鍵字引數,則應當來自類定義)。 __prepare__ 方法應該被實現為 classmethod()。 __prepare__ 所返回的名稱空間會被傳入 __new__,但是當最終的類物件被建立時,該名稱空間會被拷貝到一個新的 dict 中。
	- 如果元類沒有 __prepare__ 屬性,則類名稱空間將初始化為一個空的有序對映。

4) 執行類主體;		 
	- 類主體會以(類似於) exec(body, globals(), namespace) 的形式被執行。普通呼叫與 exec() 的關鍵區別在於當類定義發生於函式內部時,詞法作用域允許類主體(包括任何方法)引用來自當前和外部作用域的名稱。
	- 但是,即使當類定義發生於函式內部時,在類內部定義的方法仍然無法看到在類作用域層次上定義的名稱。類變數必須通過例項的第一個形參或類方法來訪問,或者是通過下一節中描述的隱式詞法作用域的 __class__ 引用。

5) 建立類物件。		 
	- 一旦執行類主體完成填充類名稱空間,將通過呼叫 metaclass(name, bases, namespace, **kwds) 建立類物件(此處的附加關鍵字引數與傳入 __prepare__ 的相同)。
	- 如果類主體中有任何方法引用了 __class__ 或 super,這個類物件會通過零引數形式的 super(). __class__ 所引用,這是由編譯器所建立的隱式閉包引用。這使用零引數形式的 super() 能夠正確標識正在基於詞法作用域來定義的類,而被用於進行當前呼叫的類或例項則是基於傳遞給方法的第一個引數來標識的。

二 class關鍵字建立類的流程分析

	class 關鍵字幫我們建立類的時候,必然幫我們呼叫了元類 `OldboyTeacher=type(...)`,那呼叫type時傳入的引數是類的三大組成部分。

1 類的三大組成部分

1).類名class_name
	class_name = 'OldboyTeacher'

2).基類們class_bases
	class_bases = (object, )

3).類的名稱空間class_dic
	類的名稱空間是執行類體程式碼而得到的

2 class關鍵字建立類的四個過程

1).先拿到類名
	class_name = "OldboyTeacher"

2).然後拿到類的基類/父類們
	class_bases = (object,)

3).再執行類體程式碼,拿到類的名稱空間
	class_dic = {}

4).呼叫元類(傳入三大要素:類名,基類,類的名稱空間) 得到一個元類的物件
	然後將元類的物件賦值給變數名OldboyTeacher,OldboyTeacher就是我們用class自定義的那個類。
	OldboyTeacher = type(class_name, class_bases, class_dic)

![](https://gitee.com/chaochaofan/img/raw/master/002 建立類的過程.png)

3 補充:exec的用法

	執行類體程式碼,拿到類的名稱空間的過程中,實際上使用了exec() 得到了class_dic
#exec:三個引數#引數一:包含一系列python程式碼的字串#引數二:全域性作用域(字典形式) ,如果不指定,預設為globals()#引數三:區域性作用域(字典形式) ,如果不指定,預設為locals()#可以把exec命令的執行當成是一個函式的執行,會將執行期間產生的名字存放於區域性名稱空間中exec(class_body,{},class_dic)

三 自定義元類

	一個類沒有宣告自己的元類,預設他的元類就是type,除了使用內建元類type,我們也可以通過繼承type來自定義元類, 然後使用metaclass關鍵字引數為一個類指定元類。
class Mymeta(type):  # 只有繼承了type類的類才是自定義的元類,否則就是一個普通類    passclass OldboyTeacher(object, metaclass=Mymeta):    school = 'oldboy'    def __init__(self, name, age):        self.name = name        self.age = age    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)# 1、先拿到一個類名:"OldboyTeacher"# 2、然後拿到類的父類:(object,)# 3、再執行類體程式碼,將產生的名字放到名稱空間中{...}# 4、呼叫元類(傳入類的三大要素:類名、基類、類的名稱空間) 得到一個元類的物件,然後將元類的物件賦值給變數名OldboyTeacher,oldboyTeacher就是我們用class自定義的那個類OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})

四 自定義元類來控制類的產生

# 自定義的元類可以控制類的產生過程,類的產生過程其實就是元類的呼叫過程,即:    OldboyTeacher=Mymeta('OldboyTeacher',(object),{...}),呼叫Mymeta會產生一個空物件OldboyTeacher,然後連同呼叫Mymeta括號內的引數一同傳給Mymeta下的__ init __ 方法,完成初始化。
import reclass Mymeta(type):  # 只有繼承了type類的類才是自定義的元類    def __init__(self, class_name, class_bases, class_dic):        # print(self)  # 類<class '__main__.OldboyTeacher'>        # print(class_name)        # print(class_bases)        # print(class_dic)        if not re.match("[A-Z]", class_name):            raise BaseException("類名必須用駝峰體")        if len(class_bases) == 0:            raise BaseException("至少繼承一個父類")        # print("文件註釋:",class_dic.get('__doc__'))        doc=class_dic.get('__doc__')        if not (doc and len(doc.strip()) > 0):            raise BaseException("必須要有檔案註釋,並且註釋內容不為空")# OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})class OldboyTeacher(object,metaclass=Mymeta):    """    adsaf    """    school = 'oldboy'    def __init__(self, name, age):        self.name = name        self.age = age    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)

五 自定義元類來控制類的呼叫

1 前置知識:__call__

	呼叫一個物件,就是觸發物件所在類中的 `__call__ `方法的執行。​	自定義元類,必須使用`__call__ `方法,不然類無法被呼叫,也沒有例項化過程中的各個步驟,以及也無法有返回值。
class Foo:    def __call__(self, *args, **kwargs):        print(self)        print(args)        print(kwargs)obj=Foo()#1、要想讓obj這個物件變成一個可呼叫的物件,需要在該物件的類中定義一個方法__call__方法,該方法會在呼叫物件時自動觸發#2、呼叫obj的返回值就是__call__方法的返回值res=obj(1,2,3,x=1,y=2)
# python官方文件解釋:# 模擬可呼叫物件object.__call__(self[, args...])# 此方法會在例項作為一個函式被“呼叫”時被呼叫;如果定義了此方法,則 x(arg1, arg2, ...) 就相當於 x.__call__(arg1, arg2, ...) 的快捷方式。

2 自定義元類來控制類OldboyTeacher的呼叫

	如果把OldboyTeacher也當作一個物件,那麼OldboyTeacher這個物件也必然存在一個`__call__ ` 。
import reclass Mymeta(type):  # 只有繼承了type類的類才是自定義的元類    def __init__(self, class_name, class_bases, class_dic):        # print(self)  # 類<class '__main__.OldboyTeacher'>        # print(class_name)        # print(class_bases)        # print(class_dic)        if not re.match("[A-Z]", class_name):            raise BaseException("類名必須用駝峰體")        if len(class_bases) == 0:            raise BaseException("至少繼承一個父類")        # print("文件註釋:",class_dic.get('__doc__'))        doc = class_dic.get('__doc__')        if not (doc and len(doc.strip()) > 0):            raise BaseException("必須要有檔案註釋,並且註釋內容不為空")    # res = OldboyTeacher('egon',18)    def __call__(self, *args, **kwargs):        # 1、先建立一個老師的空物件        tea_obj = object.__new__(self)        # 2、呼叫老師類內的__init__函式,然後將老師的空物件連同括號內的引數的引數一同傳給__init__        self.__init__(tea_obj, *args, **kwargs)        tea_obj.__dict__ = {"_%s__%s" %(self.__name__,k): v for k, v in tea_obj.__dict__.items()}        # 3、將初始化好的老師物件賦值給變數名res        return tea_obj# OldboyTeacher = Mymeta("OldboyTeacher",(object,),{...})class OldboyTeacher(object, metaclass=Mymeta):    """    adsaf    """    school = 'oldboy'    #            tea_obj,'egon',18    def __init__(self, name, age):        self.name = name  # tea_obj.name='egon'        self.age = age  # tea_obj.age=18    def say(self):        print('%s says welcome to the oldboy to learn Python' % self.name)res = OldboyTeacher('egon', 18)print(res.__dict__)# print(res.name)# print(res.age)# print(res.say)# 呼叫OldboyTeacher類做的事情:# 1、先建立一個老師的空物件# 2、呼叫老師類內的__init__方法,然後將老師的空物件連同括號內的引數的引數一同傳給__init__# 3、將初始化好的老師物件賦值給變數名res

3 呼叫類做的三件事情:

1).產生一個空物件obj;2).呼叫類內的`__init__ `方法,然後將空物件連同括號內的引數的引數一同傳給`__init__ `;3).將初始化好的物件賦值給變數名obj。

六 單例模式

1 什麼是單例模式

	單例模式(Singleton Pattern) 是最簡單的設計模式之一。這種型別的設計模式屬於建立型模式,它提供了一種建立物件的最佳方式。​	這種模式涉及到一個單一的類,該類負責建立自己的物件,同時確保只有單個物件被建立。這個類提供了一種訪問其唯一的物件的方式,可以直接訪問,不需要例項化該類的物件。

2 為何要有單例模式

## 1.意圖:	保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。## 2.主要解決:	一個全域性使用的類頻繁地建立與銷燬。

3 如何使用單例模式

## 1.何時使用:	當您想控制例項數目,節省系統資源的時候。## 2.如何解決:	判斷系統是否已經有這個單例,如果有則返回,如果沒有則建立。## 3優點:    (1) 在記憶體裡只有一個例項,減少了記憶體的開銷,尤其是頻繁的建立和銷燬例項(比如管理學院首頁頁面快取) 。    (2) 避免對資源的多重佔用(比如寫檔案操作) 。## 4缺點:	沒有介面,不能繼承,與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來例項化。

4 三種實現單例模式的方法

# 如果我們從配置檔案中讀取配置來進行例項化,在配置相同的情況下,就沒必要重複產生物件浪費記憶體了# settings.py檔案內容如下HOST = '1.1.1.1'PORT = 3306

4.1 實現方式一:classmethod

	可以使用classmethod,使用類呼叫類內的單例函式,來完成唯一的值產生。
import settingsclass MySQL:    __instance = None  # 設定變數,    def __init__(self, ip, port):        self.ip = ip        self.port = port    @classmethod    def singleton(cls):        if cls.__instance:  # 如果已例項化,則不再進行新的例項化            return cls.__instance        cls.__instance = cls(settings.IP, settings.PORT)  # 未例項化,開始例項化        return cls.__instance# obj1=MySQL("1.1.1.1",3306)# obj2=MySQL("1.1.1.2",3306)# print(obj1)# print(obj2)obj3 = MySQL.singleton()print(obj3)obj4 = MySQL.singleton()print(obj4)

4.2 實現方式二:元類

import settingsclass Mymeta(type):    __instance = None    def __init__(self, class_name, class_bases, class_dic):  # 定義類Mysql時就觸發        # 事先先從配置檔案中取配置來造一個Mysql的例項出來        self.__instance = object.__new__(self)  # Mysql類的物件        self.__init__(self.__instance, settings.IP, settings.PORT)  # 初始化物件        # 上述兩步可以合成下面一步        # self.__instance=super().__call__(*args,**kwargs)    def __call__(self, *args, **kwargs):  # Mysql(...)時觸發        if args or kwargs:  # args或kwargs內有值            obj = object.__new__(self)            self.__init__(obj, *args, **kwargs)            return obj        else:            return self.__instance# MySQL=Mymeta(...)class MySQL(metaclass=Mymeta):    def __init__(self, ip, port):        self.ip = ip        self.port = port# obj1 = MySQL("1.1.1.1", 3306)# obj2 = MySQL("1.1.1.2", 3306)# print(obj1)# print(obj2)obj3 = MySQL()obj4 = MySQL()print(obj3 is obj4)

4.3 實現方式三:裝飾器

import settingsdef outter(func):  # func = MySQl類的記憶體地址    _instance = func(settings.IP,settings.PORT)    def wrapper(*args,**kwargs):        if args or kwargs:            res=func(*args,**kwargs)            return res        else:            return _instance    return wrapper@outter  # MySQL=outter(MySQl類的記憶體地址)  # MySQL=》wrapperclass MySQL:    def __init__(self, ip, port):        self.ip = ip        self.port = port# obj1 = MySQL("1.1.1.1", 3306)# obj2 = MySQL("1.1.1.2", 3306)# print(obj1)# print(obj2)obj3 = MySQL()obj4 = MySQL()print(obj3 is obj4)

4.4 方式二類中的 __call__方法做的三件事情:

    1).呼叫`__new__`產生一個空物件obj;    2).呼叫`__init__`初始化空物件obj,然後將空物件連同括號內的引數的引數一同傳給`__init__`;    3).將初始化好的物件賦值給變數名obj。

七 瞭解:屬性查詢

1 物件OldboyTeacher裡的屬性查詢

	我們用class自定義的類也全都是物件(包括object類本身也是元類type的 一個例項,可以用type(object)檢視) ,如果把類當成物件去看,將下述繼承應該說成是:物件OldboyTeacher繼承物件Foo,物件Foo繼承物件Bar,物件Bar繼承物件object
class Mymeta(type): #只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類    n=444    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>        obj=self.__new__(self)        self.__init__(obj,*args,**kwargs)        return objclass Bar(object):    n=333class Foo(Bar):    n=222class OldboyTeacher(Foo,metaclass=Mymeta):    n=111    school='oldboy'    def __init__(self,name,age):        self.name=name        self.age=age    def say(self):        print('%s says welcome to the oldboy to learn Python' %self.name)print(OldboyTeacher.n) #自下而上依次註釋各個類中的n=xxx,然後重新執行程式,發現n的查詢順序為OldboyTeacher->Foo->Bar->object->Mymeta->type
# 屬性查詢應該分兩層,一層是物件層(基於c3演算法的MRO) 的查詢,另外一個則是元類(即元類層) 的查詢。

![](https://gitee.com/chaochaofan/img/raw/master/003 屬性查詢1.png)

![](https://gitee.com/chaochaofan/img/raw/master/004 屬性查詢2.png)

#查詢順序:#1、先物件層:OldoyTeacher->Foo->Bar->object#2、然後元類層:Mymeta->type

2 元類Mymeta中__call__裡的self.__new__的查詢

class Mymeta(type):     n=444    def __call__(self, *args, **kwargs): #self=<class '__main__.OldboyTeacher'>        obj=self.__new__(self)        print(self.__new__ is object.__new__) #Trueclass Bar(object):    n=333    # def __new__(cls, *args, **kwargs):    #     print('Bar.__new__')class Foo(Bar):    n=222    # def __new__(cls, *args, **kwargs):    #     print('Foo.__new__')class OldboyTeacher(Foo,metaclass=Mymeta):    n=111    school='oldboy'    def __init__(self,name,age):        self.name=name        self.age=age    def say(self):        print('%s says welcome to the oldboy to learn Python' %self.name)    # def __new__(cls, *args, **kwargs):    #     print('OldboyTeacher.__new__')OldboyTeacher('egon',18) 	# 觸發OldboyTeacher的類中的__call__方法的執行,進而執行self.__new__開始查詢
# 總結:	Mymeta下的__call__裡的self.__new__在OldboyTeacher、Foo、Bar裡都沒有找到__new__的情況下,會去找object裡的__new__,而object下預設就有一個__new__,所以即便是之前的類均未實現__new__,也一定會在object中找到一個,根本不會、也根本沒必要再去找元類Mymeta->type中查詢__new__