[Python學習日記-70] 元類
簡介
什么是元類
關鍵字 class 創建類的流程分析
自定義元類控制類的創建
自定義元類控制類的調用
自定義元類的屬性查找
自定義元類的應用與練習
簡介
? ? ? ? 在上一篇章當中我們已經了解了面向對象的各種內置函數了,本篇我們將講述“元類”,它是 Python 面向對象編程的深層次知識,學會了元類可以做到很多神奇的姿勢,這次就帶大家一起來探討一下什么是元類,我們應該如何定制自己的元類,我們應該怎么調用我們自己的元類。
什么是元類
? ? ? ?在介紹什么是元類之前我們先定義一個類作為我們的分析對象,如下
class Chinese: # Python3 中默認就是新式類,即 Chinese(object)country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)c1 = Chinese('jove',28)
print(type(c1)) # 查看對象c1的類是 ——> <class '__main__.Chinese'>
代碼輸出如下:
????????在前面的許多篇博客當中我們都提過一個特性:Python 中的一切皆為對象;而所有的對象都是實例化而得到的,就像上面的 c1 是調用類 Chinese 實例化后得到的,那對象可以怎么用呢?對象有以下四點特性:
- 都可以被引用,即 x = obj
- 都可以當作函數的參數傳入
- 都可以作當函數的返回值
- 都可以當作容器類的元素,即 l = [func,time,obj,1,...]
? ? ? ? 只要是對象就會擁有上面的四點特性,上面的 c1 很明顯是一個對象了,其實我們所創建的類 Chinese 的本質也是一個對象,它也擁有以上四點特性。
????????既然所有的對象都是調用類得到的,那么 Chinese 也必然是調用了一個類得到的,我們把這個被 Chinese 調用的類就稱為元類,總的來說產生 Chinese 的過程一定發生了:Chinese = 元類(...),即產生類的類就是元類;我們可以通過以下代碼查看 Chinese 的元類是什么,如下
print(type(Chinese))
代碼輸出如下:
? ? ? ? 從輸出來看 Chinese 的產生是調用了 type 這個元類,即默認的元類為 type,類與對象的產生過程如下圖所示
關鍵字 class 創建類的流程分析
? ? ? ? 在分析創建類的流程之前我們要先補充一下 exec 方法的用法,exec 方法在創建類時起到了關鍵的作用,它是用于執行動態生成的代碼,并會生成相應的作用域/名稱空間,exec 方法的語法如下
exec(code,globals_dic,locals_dic)
- code:一系列python代碼的字符串
- globals_dic:全局作用域(字典形式),如果不指定,默認為globals()
- ocals_dic:局部作用域(字典形式),如果不指定,默認為locals()
? ? ? ? 我們可以把 exec 方法的執行當成是一個函數的執行,會將執行期間產生的名字存放于局部名稱空間中,演示代碼如下
g = {'x':1,'y':2
}l = {}exec("""
global x,m
x = 10
m = 100
z = 3
""",g,l)print(g)
print(l)
代碼輸出如下:
{'x': 10, 'y': 2, ..., 'm': 100}
{'z': 3}
? ? ? ? 補充完 exec 方法的使用后我們書接上文,前面我們說到,使用關鍵字 class 創建的類 Chinese 本身也是一個對象,負責產生該對象的類被我們稱之為元類,而在 Python 中內置的元類就是 type。關鍵字 class 在幫我們創建類的時候必然會幫我們調用元類 type,即 Chinese = type(...),元類 type 進行實例化的時候會依次傳入以下三個參數,這三個參數就是類的關鍵組成部分,分別是
- 類名:class_name = 'Chinese'
- 基類(父類)們:class_bases = (object,)
- 類的名稱空間:class_dic,類的名稱空間是執行類體代碼而得到的
? ? ? ? 總的來說,關鍵字 class?幫我們創建一個類分為以下四個過程:
? ? ? ? 到這里我們知道了,其實我們用關鍵字 class 創建的類也只是一個用元類 type 創建的對象而已,那也就是說其實我們也可以自己用元類 type 來創建類,并不需要使用關鍵字 class,總的來說在 Python 中定義類有兩種方式:
- 方式一:關鍵字 class 創建
- 方式二:由元類 type 創建
? ? ? ? 為了驗證我們分析的正確性,我們分別使用兩種創建類的方式來創建兩個類來對比一下,代碼如下
# 定義類的兩種方式:
# 方式一: class
class Chinese: # Chinese = type(...)country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)obj = Chinese('jove',28)
print(obj,obj.name,obj.age)
# print(type(obj.talk))
# print(Chinese.__bases__)# 方式二: type
# 定義類的三要素
class_name = 'Chinese' # 類名
class_bases = (object,) # 基類(父類)
# 類里的代碼
class_body = """
country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)
"""class_dic = {}
exec(class_body,globals(),class_dic) # 執行類里的代碼,并把類里面的屬性(非功能性代碼)都放到dict --> locals() 里面
# print(class_dic)Chinese1 = type(class_name,class_bases,class_dic) # 最后傳入所需的要素到type()當中obj1 = Chinese1('jove',28)
print(obj1,obj1.name,obj1.age)
代碼輸出如下:
自定義元類控制類的創建
? ? ? ? 經過前面一大輪的分析,我們已經清楚了 Python 當中默認的元類是 type,而我們能使用?metaclass 關鍵字參數為一個類指定元類,在默認的情況下如下
class Chinese(object,metaclass=type): # 默認metaclass就等于typecountry = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)
? ? ? ? 而我們可以通過繼承 type 來自定義元類,然后使用 metaclass 關鍵字參數為一個類指定自定義元類即可,如下
class Mymeta(type): # 只有繼承了type類才能稱之為一個元類,否則就是一個普通的自定義類def __init__(self,class_name,class_bases,class_dic):print(class_name)print(class_bases)print(class_dic)super(Mymeta,self).__init__(class_name,class_bases,class_dic) # 重用父類的功能class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)
代碼輸出如下:
? ? ? ? 從輸出可以看出,在創建類 Chinese 的時候同時調用了自定義元類 Mymeta,從這可以看出自定義元類可以控制類的產生過程,而類的產生過程其實就是元類的調用過程,即?Chinese = Mymeta(class_name,class_bases,class_dic),在調用?Mymeta 時會先產生一個空對象 Chinese,然后連同調用 Mymeta 括號內的參數一同傳給 Mymeta 下的 __init__ 方法來完成初始化,這樣我們可以基于這個調用機制來做一些關于創建類的限制,例如限制類名的書寫格式,代碼如下
class Mymeta(type):def __init__(self,class_name,class_bases,class_dic):if not class_name.istitle(): # 類名開頭必須為大寫raise TypeError('類名的首字母必須大寫') # 異常處理后面會有專門的篇章介紹if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果為空則自帶布爾值 falseraise TypeError('類必須寫注釋,且不能為空')super(Mymeta,self).__init__(class_name,class_bases,class_dic)class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)'''中國人的類'''country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)cn = Chinese('jove',28)
cn.talk()
代碼輸出如下:
????????當代碼沒有按照要求類名的首字母大寫時
? ? ? ? 當代碼當中沒有注釋說明時
? ? ? ? 當所有要求都符合時
自定義元類控制類的調用
????????在學習自定義元類的調用之前我們需要先掌握?__call__?方法的使用,這在之前已經介紹過了,可以點擊鏈接查看。了解完 __call__ 方法之后,我們知道調用一個對象,就是觸發對象所在類中的 __call__ 方法的執行,如果把?Chinese 也當做一個對象,那么在 Chinese 這個對象的類中也必然存在一個 __call__ 方法,即 Chinese 的元類里面也應該有一個 __call__ 方法,會在 Chinese() 調用時觸發,如下
class Mymeta(type):def __init__(self,class_name,class_bases,class_dic):if not class_name.istitle(): # 類名開頭必須為大寫raise TypeError('類名的首字母必須大寫') # 異常處理后面會有專門的篇章介紹if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果為空則自帶布爾值 falseraise TypeError('類必須寫注釋,且不能為空')super(Mymeta,self).__init__(class_name,class_bases,class_dic)def __call__(self, *args, **kwargs): # 如果沒有寫這個,將會找父類的__call__方法 obj = Chinese('egon', 18)print(self) # self = Chineseprint(args) # arge = ('jove',)print(kwargs) # kwarge = {'age': 28}class Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)'''中國人的類'''country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)cn = Chinese('jove',28) # Chinese.__call__(Chinese,'jove',28)
print(cn)
代碼輸出如下:
? ? ? ? 從輸出結果來看,調用 Chinese 對象的時候就是在調用 Chinese 類中的 __call__ 方法,然后會把 Chinese 傳遞給 self,而溢出的位置參數和關鍵字參數分別傳遞給?*args 和 **kwargs,最后調用 Chinese 的返回值就是調用的 __call__ 方法的返回值,這里的 __call__ 方法沒有指定返回值所以打印 cn 時就是 None。
? ? ? ? 很明顯的看出,我們自定義的 __call__ 還沒有實現實例化對象 cn 的功能,那應該怎么做呢?默認地,在調用 cn?= Chinese('jove',28) 時 __call__ 應該做以下三件事:
- 產生一個空對象 obj
- 調用 __init__ 方法初始化對象 obj
- 返回初始化好的 obj
? ? ? ? 實現代碼如下
class Mymeta(type):def __init__(self,class_name,class_bases,class_dic):if not class_name.istitle(): # 類名開頭必須為大寫raise TypeError('類名的首字母必須大寫') # 異常處理后面會有專門的篇章介紹if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果為空則自帶布爾值 falseraise TypeError('類必須寫注釋,且不能為空')super(Mymeta,self).__init__(class_name,class_bases,class_dic)def __call__(self, *args, **kwargs): # 如果沒有寫這個,將會找父類的__call__方法 obj = Chinese('egon', 18)# 第一件事: 調用__new__造出一個空對象objobj = object.__new__(self) # 此處的self是類Chinese,必須傳參,代表創建一個Chinese的對象obj# 第二件事: 調用__init__方法初始化空對象objself.__init__(obj, *args, **kwargs)# 第三件事: 返回初始化好的 objreturn objclass Chinese(object,metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)'''中國人的類'''country = 'China'def __init__(self,name,age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)cn = Chinese('jove',28) # Chinese.__call__(Chinese,'jove',28)
print(cn)
代碼輸出如下:
? ? ? ? ?從輸出結果來看已經看到 cn 已經變成了類 Chinese 的一個對象了,這個時候已經完成了實例化,而上面代碼當中的 __call__ 其實只相當于一個模版而已,我們還能在此基礎上改寫 __call__ 的邏輯從而控制調用 Chinese 的過程,例如把 Chinese 實例化的對象的所有屬性都變成私有屬性,如下
class Mymeta(type):def __init__(self, class_name, class_bases, class_dic):if not class_name.istitle(): # 類名開頭必須為大寫raise TypeError('類名的首字母必須大寫') # 異常處理后面會有專門的篇章介紹if '__doc__' not in class_dic or not class_dic['__doc__'].strip(): # strip()如果為空則自帶布爾值 falseraise TypeError('類必須寫注釋,且不能為空')super(Mymeta, self).__init__(class_name, class_bases, class_dic)def __call__(self, *args, **kwargs): # 如果沒有寫這個,將會找父類的__call__方法 obj = Chinese('egon', 18)# 第一件事: 調用__new__造出一個空對象objobj = object.__new__(self) # 此處的self是類Chinese,必須傳參,代表創建一個Chinese的對象obj# 第二件事: 調用__init__方法初始化空對象objself.__init__(obj, *args, **kwargs)# 在初始化之后,obj.__dict__里就有值了print(obj.__dict__)obj.__dict__ = {'_%s__%s' % (self.__name__, k): v for k, v in obj.__dict__.items()}# 第三件事: 返回初始化好的 objreturn objclass Chinese(object, metaclass=Mymeta): # Chinese = Mymeta(class_name,class_bases,class_dic)'''中國人的類'''country = 'China'def __init__(self, name, age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)cn = Chinese('jove', 28) # Chinese.__call__(Chinese,'jove',28)
print(cn.__dict__)
代碼輸出如下:
自定義元類的屬性查找
? ? ? ? 到這里基本就介紹完元類了,在學習完元類之后再來看看結合了繼承和元類之后的屬性查找應該是怎么樣的一個查找順序呢?我們先來寫一段代碼,如下
class Mymeta(type):n=444def __call__(self, *args, **kwargs):obj=self.__new__(self)self.__init__(obj,*args,**kwargs)return objclass Bar(object):n=333class Foo(Bar):n=222class Chinese(Foo,metaclass=Mymeta):n=111country = 'China'def __init__(self, name, age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)print(Chinese.n) # 自下而上依次注釋各個類中的n=xxx,然后重新運行程序,發現n的查找順序為Chinese -> Foo -> Bar -> object -> Mymeta -> type
代碼輸出如下:
? ? ? ? 注釋掉 Chinese 下的 n=111 后
·? ? ? ? 注釋掉 Foo 下的 n=222 后
? ? ? ? 注釋掉 Bar 下的 n=333 后
? ? ? ? 最后注釋掉 Mymeta 下的 n=444 后直接報錯找不到了
????????在前面我們學習過繼承的實現原理,如果把類當成對象去看,上面代碼的繼承關系是:對象 Chinese 繼承對象 Foo,對象 Foo 繼承對象 Bar,對象 Bar 繼承對象 object。我們應該把屬性的查找分成兩層,一層是對象層的查找,即基于 c3 算法的 MRO?方法調用順序;另一層則是類層的查找,即對元類層的查找,查找順序如下
當對對象中的屬性進行查找時會按以下順序進行查找:
- 對象層:Chinese -> Foo -> Bar -> object
- 元類層:Mymeta -> type
? ? ? ? 通過上面的分析,我們現在知道在屬性查找的時候元類也會參與其中,我們在之前使用 __call__ 方法實現實例化的時候用到了 self.__new__,下面我們來分析一下這個 __new__ 到底是調用了哪里的,我們先寫下一段代碼運行看看,如下
class Mymeta(type):n=444def __call__(self, *args, **kwargs):obj=self.__new__(self)print(self.__new__ is object.__new__) # 當前面的__new__都注釋掉之后這個就是True了class 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 Chinese(Foo,metaclass=Mymeta):n=111country = 'China'def __init__(self, name, age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)# def __new__(cls, *args, **kwargs):# print('Chinese.__new__')Chinese('jove',28)
代碼輸出如下:
? ? ? ? 上面代碼中注釋的 __new__ 方法可以自己運行一下,這樣可以更加清楚的看到 self.__new__ 的查找情況 。一番操作后發現 Mymeta 下的 __call__ 里的 self.__new__ 的查找順序是 Chinese ->?Foo -> Bar -> object,而且經過?self.__new__ 和 object.__new__ 的比對之后發現這兩個是一樣的,那就是說 self.__new__ 的查找并沒有找到元類當中,而是會去找 object 里的 __new__,而 object 下默認就有一個 __new__,所以即便是之前的類均未實現 __new__,也一定會在 object 中找到一個,根本不會再去找元類 Mymeta 和 type 中查找 __new__。
? ? ? ? 那我們是否可以在元類的 __call__ 中用 object.__new__(self) 代替 self.__new__(self) 去造對象呢?原則上是可以的,因為通過屬性查找最終還是會找到?object.__new__,但是并不推薦這樣做,因為直接使用?object.__new__ 會直接跳過之前的 Chinese、Foo 和 Bar 三個類的檢索,如果后期在他們當中想做 __new__ 的自定義的話會造成一定的麻煩。
? ? ? ? 那什么情況下才會去元類層查找 __new__ 呢?在產生類 Chinese 的過程就是在調用 Mymeta,而 Mymeta 也是 type 類的一個對象,那么 Mymeta 之所以可以調用,一定是在元類 type 中也有一個 __call__ 方法,而這個 __call__ 方法也同樣需要做至少三件事,如下
class type:def __call__(self, *args, **kwargs): # self=<class '__main__.Mymeta'>obj=self.__new__(self,*args,**kwargs) # 產生Mymeta的一個對象self.__init__(obj,*args,**kwargs) return obj
? ? ? ? 這個時候 type 中的?self.__new__ 進行檢索的時候就會先對 Mymeta 進行檢索,然后再對 type 進行檢索,所以我們可以通過這個邏輯來定制我們的自定義元類 Mymeta,如下
class Mymeta(type):n=444def __new__(cls, *args, **kwargs):obj=type.__new__(cls,*args,**kwargs) # 必須按照這種傳值方式print(obj.__dict__)return obj # 只有在返回值是type的對象時,才會觸發下面的__init__# return 123def __init__(self,class_name,class_bases,class_dic):print('run。。。')class Chinese(Foo,metaclass=Mymeta):n=111country = 'China'def __init__(self, name, age):self.name = nameself.age = agedef talk(self):print('%s is talking' % self.name)print(type(Mymeta))
代碼輸出如下:
? ? ? ? 當返回 type 的對象時
? ? ? ? 當返回的不是 type 的對象時
自定義元類的應用與練習
一、單例模式
? ? ? ? 在詳細介紹完元類之后我們再來講講元類的一些實際應用——單例模式。什么是單例模式呢?在 Python 當中我們定義兩個不同的變量,但是值是一樣的,如下
obj1 = int(1) # obj1 = 1
obj2 = int(1) # obj2 = 1
print(obj1 is obj2)
代碼輸出如下:
? ? ? ? obj1 和 obj2 兩個對象是指向相同的內存地址的,而單例模式要做的事就是把相同特征的對象只產生一個內存地址,從而節約內存空間,下面我們會用兩種方式來實現單例模式,如下
配置文件 setting.py:
HOST = '127.0.0.1'
PORT = 1000
實現方式一:不使用元類
import settingsclass MySQL:__instance = Nonedef __init__(self,host,port):self.host = hostself.port = port@classmethoddef singleton(cls):if not cls.__instance: # 這樣的賦值方法只適用于self的特征寫死的情況下obj = cls(settings.HOST,settings.PORT)cls.__instance = objreturn cls.__instancedef conn(self):passdef execute(self):pass# 對象的特征(屬性)不同,內存地址不同
obj1 = MySQL('1.1.1.2',3306)
obj2 = MySQL('1.1.1.3',3307)
print(obj1 is obj2)# 對象的特征(屬性)相同,內存地址相同
obj1 = MySQL.singleton()
obj2 = MySQL.singleton()
print(obj1 is obj2)
代碼輸出如下:
實現方式二:使用元類來實現
import settingsclass Mymeta(type):def __init__(self,name,bases,dic): #定義類Mysql時就觸發# 事先先從配置文件中取配置來造一個Mysql的實例出來self.__instance = object.__new__(self) # 產生對象self.__init__(self.__instance, settings.HOST, settings.PORT) # 初始化對象# 上述兩步可以合成下面一步# self.__instance=super().__call__(*args,**kwargs)super().__init__(name,bases,dic)def __call__(self, *args, **kwargs): #Mysql(...)時觸發if args or kwargs: # args或kwargs內有值obj=object.__new__(self)self.__init__(obj,*args,**kwargs)return objreturn self.__instanceclass Mysql(metaclass=Mymeta):def __init__(self,host,port):self.host=hostself.port=portobj1=Mysql() # 沒有傳值則默認從配置文件中讀配置來實例化,所有的實例應該指向一個內存地址
obj2=Mysql()
obj3=Mysql()print(obj1 is obj2 is obj3)obj4=Mysql('1.1.1.4',3307)
print(obj4 is obj1)
代碼輸出如下:
二、在元類中控制把自定義類的數據屬性都變成大寫
class Mymeta(type):def __new__(cls, class_name, class_base, class_dict):update_class_dict = {}''.endswith('__')for k in class_dict:if not callable(class_dict[k]) and not k.startswith('__') and not k.startswith('_'):update_class_dict[k.upper()] = class_dict[k]else:update_class_dict[k] = class_dict[k]return type.__new__(cls, class_name, class_base, update_class_dict)class Chinese(metaclass=Mymeta):country = 'China'tag = 'Legend of the Dragon' # 龍的傳人__ismarry = 'yes'def __init__(self):self.name = 'Zou'def walk(self):print('%s is walking' % self.name)print(Chinese.__dict__)
代碼輸出如下:
{'__module__': '__main__', 'COUNTRY': 'China', 'TAG': 'Legend of the Dragon', '_Chinese__ismarry': 'yes', '__init__': <function Chinese.__init__ at 0x000001A082A48B80>, 'walk': <function Chinese.walk at 0x000001A082A499E0>, '__dict__': <attribute '__dict__' of 'Chinese' objects>, '__weakref__': <attribute '__weakref__' of 'Chinese' objects>, '__doc__': None}
三、在元類中控制自定義的類無需 __init__ 方法
- 元類幫其完成創建對象,以及初始化操作
- 要求實例化時傳參必須為關鍵字形式,否則拋出異常 TypeError: must use keyword argument
- key 作為用戶自定義類產生對象的屬性,且所有屬性變成大寫
class Mymeta(type):def __call__(self, *args, **kwargs):if args:raise TypeError('must use keyword argument for key function')obj = object.__new__(self)for k in kwargs:obj.__dict__[k.upper()] = kwargs[k]return objclass Chinese(metaclass=Mymeta):country = 'China'tag = 'Legend of the Dragon' # 龍的傳人__ismarry = 'yes'def walk(self):print('%s is walking' % self.name)obj1 = Chinese(name = 'jove',age = 28)
print(obj1.__dict__)
代碼輸出如下:
四、在元類中控制自定義的類產生的對象相關的屬性全部為隱藏屬性
class Mymeta(type):def __call__(self, *args, **kwargs):# 控制Chinese對象的創建過程if args:raise TypeError('must use keyword argument for key function')obj = object.__new__(self)for k in kwargs:obj.__dict__['_%s__%s' % (self.__name__, k)] = kwargs[k]return objclass Chinese(metaclass=Mymeta):country = 'China'tag = 'Legend of the Dragon' # 龍的傳人__ismarry = 'yes'def walk(self):print('%s is walking' % self.name)p = Chinese(name = 'jove',age = 28)
print(p.__dict__)
代碼輸出如下: