python3高階知識--元類(metaclass)深度剖析
一、簡介
在面向物件的程式設計中類和物件是其重要角色,我們知道物件是由類例項化而來,那麼類又是怎麼生成的呢?答案是通過元類。本篇文章將介紹元類相關知識,並剖析元類生成類的過程,以及元類的使用等內容,希望能幫助到正在學習python的同仁。
一、一切皆物件
在python中有這樣一句話“一切皆物件”,沒錯你所知道的dict、class、int、func等等都是物件,讓我們來看以下一段程式碼來進行說明:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # Author:wd class Foo(object):pass def func(): print('func') print(Foo.__class__) print(func.__class__) print(int.__class__) print(func.__class__.__class__) 結果: <class 'type'> <class 'function'> <class 'type'> <class 'type'>
說明:__class__方法用於檢視當前物件由哪個類生成的,正如結果所見其中Foo和int這些類(物件)都是由type建立,而函式則是由function類建立,而function類則也是由type建立,究其根本所有的這些類物件都是由type穿件。這裡的type就是python內建的元類,接下來談談type。
二、關於type
上面我們談到了所有的類(物件)都是由type生成,那麼不妨我們看看type定義,以下是python3.6中內建type定義部分摘抄:
class type(object): """ type(object_or_name, bases, dict) type(object) -> the object's type type(name, bases, dict) -> a new type """ def mro(self): # real signature unknown; restored from __doc__""" mro() -> list return a type's method resolution order """ return []
從描述資訊中我們可以看到,type(object)->返回物件type型別,也就是我們常常使用該方法判斷一個物件的型別,而type(name, bases, dict) -> 返回一個新的類(物件)。 讓我們詳細描述下這個語法:
type(類名,該類所繼承的父類元祖,該類對應的屬性字典(k,v))
利用該語法我們來穿件一個類(物件)Foo:
Foo=type('Foo',(object,),{'Name':'wd'}) print(Foo) print(Foo.Name) 結果: <class '__main__.Foo'> wd
當然也可以例項化這個類(物件):
Foo=type('Foo',(object,),{'Name':'wd'}) obj=Foo() print(obj.Name) print(obj) print(obj.__class__) 結果: wd <__main__.Foo object at 0x104482438> <class '__main__.Foo'>
這樣建立方式等價於:
class Foo(object): Name='wd'
其實上面的過程也就是我們使用class定義類生成的過程,而type就是python中的元類。
三、元類
什麼是元類
經過以上的介紹,說白了元類就是建立類的類,有點拗口,姑且把這裡稱為可以建立類物件的類。列如type就是元類的一種,其他的元類都是通過繼承type或使用type生成的。通過元類我們可以控制一個類建立的過程,以及包括自己定製一些功能。 例如,下面動態的為類新增方法:
def get_name(self): print(self.name) class MyType(type): def __new__(cls, cls_name, bases, dict_attr): dict_attr['get_name'] = get_name #將get_name 作為屬性新增到類屬性中 return super(MyType, cls).__new__(cls, cls_name, bases, dict_attr) class Foo(metaclass=MyType): def __init__(self, name): self.name = name obj = Foo('wd') obj.get_name()#呼叫該方法 結果: wd
以上示例說明: 1.MyType是繼承了type,也就是說繼承了其所有的功能與特性,所以它也具有建立類的功能,所以它也是元類; 2.類Foo中使用了metaclass關鍵字,表明該類由MyType進行建立。 3.建立Foo類時候會先執行MyType的__new__方法(後續會這些方法進行更詳細的說明),並接受三個引數,cls_name, bases, dict_attr,在改方法中我們在類屬性字典中添加了get_name屬性,並將它與函式繫結,這樣生成的類中就有了該方法。
使用元類
瞭解類元類的作用,我們知道其主要目的就是為了當建立類時能夠根據需求改變類,在以上的列子中我們介紹了使用方法,其中就像stackoverflow中關於對元類的使用建議一樣,絕大多數的應用程式都非必需使用元類,並且使用它可能會對你的程式碼帶來一定的複雜性,但是就元類的使用而言其實很簡單,其場景在於:
1.對建立的類進行校驗(攔截);
2.修改類;
3.為該類定製功能;
使用元類是時候經典類和新式類時候有些不同,新式類通過引數metaclass,經典類通過__metaclass__屬性:
class Foo(metaclass=MyType): #新式類 pass class Bar: # 經典類 __metaclass__ = MyType pass
在解釋元類的時候有提到過,元類可以是type,也可以是繼承type的類,當然還可以是函式,只要它是可呼叫的。但是有個必要的前提是該函式使用的是具有type功能的函式,否則生成的物件可能就不是你想要的(在後續的原理在進行講解)。以下示例將給出使用函式作為元類來建立類:
def class_creater(cls_name, bases, dict_attr): return type(cls_name, bases, dict_attr) class Foo(metaclass=class_creater): def __init__(self,name): self.name=name obj=Foo('wd') print(obj.name) #wd
原理
當我們使用class定義類時候,它會執行以下步驟:- 獲取類名,以示例中class Foo為例,類名是Foo。
- 獲取父類,預設object,以元祖的形式,如(object,Foo)
- 獲取類的屬性字典(也叫名稱空間)
- 將這三個引數傳遞給元類(也就是metaclass引數指定的類),如果沒有metaclass引數則使用type生成類。
元類建立類的過程
其實如果你對面向物件非常熟悉的話,其過程也是非常容易理解的,在介紹類生成的過程之前,我們需要對三個方法做充分的理解:__init__、__new__、__call__。- __init__ :通常用於初始化一個新例項,控制這個初始化的過程,比如新增一些屬性, 做一些額外的操作,發生在類例項被建立完以後。它是例項級別的方法。觸發方式為:類()
- __new__ :通常用於控制生成一個類例項的過程,依照Python官方文件的說法,__new__方法主要是當你繼承一些不可變的時(比如int, str, tuple), 提供給你一個自定義這些類的例項化過程的途徑。它是類級別的方法。
- __call__ :當類中有__call__方法存在時候,該類實列化的物件就是可呼叫的,觸發方式為:物件()。
class Foo(object): def __init__(self, name): print('this is __init__') self.name = name def __new__(cls, *args, **kwargs): print('this is __new__') return object.__new__(cls) def __call__(self, *args, **kwargs): print("this is __call__") obj=Foo('wd') # 例項化 obj() # 觸發__call__ 結果: this is __new__ this is __init__ this is __call__
有了這個知識,再來看看使用元類生成類,以下程式碼定義來一個元類繼承來type,我們重寫__new__和__init__方法(其實什麼也沒幹),為了說明類的生成過程:
class MyType(type): def __init__(self, cls_name, bases, cls_attr): print("Mytype __init__", cls_name, bases) def __new__(cls, cls_name, bases, cls_attr): print("Mytype __new__", cls_name, bases) return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr) class Foo(metaclass=MyType): def __init__(self, name): print('this is __init__') self.name = name def __new__(cls, *args, **kwargs): print('this is __new__') return object.__new__(cls) print("line -------") obj = Foo('wd') # 例項化 結果: Mytype __new__ Foo () Mytype __init__ Foo () line ------- this is __new__ this is __init__
解釋說明:
- 首先metaclass接受一個可呼叫的物件,而在這裡該物件是一個類,也就是說會執行MyType(),並把cls_name,bases,cls_attr傳遞給MyType,這不就是MyType的示例化過程嗎,所以你在結果中可以看到,分割線是在"Mytype __new__”和“Mytype __init__”之後輸出,接下來在看MyType。
- MyType元類的例項化過程和普通類一樣,先執行自己__new__方法,在執行自己的__init__方法,在這裡請注意__new__方法是控制MyType類生成的過程,而__init__則是例項化過程,用於生成類Foo。這樣一來是不是對類的生成過程有了非常深刻的認識。
class MyType(type): def __init__(self, cls_name, bases, cls_attr): print("Mytype __init__", cls_name, bases) def __new__(cls, cls_name, bases, cls_attr): print("Mytype __new__", cls_name, bases) return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr) def __call__(self, *args, **kwargs): print('Mytype __call__') class Foo(metaclass=MyType): def __init__(self, name): print('this is __init__') self.name = name def __new__(cls, *args, **kwargs): print('this is __new__') return object.__new__(cls) def __call__(self, *args, **kwargs): print("this is __call__") print("before -------") obj = Foo('wd') # 例項化 print("after -------") print(obj) 結果: Mytype __new__ Foo () Mytype __init__ Foo () before ------- Mytype __call__ after ------- None
你會發現,當Foo例項化時候執行了元類的__call__,你從python的一切皆物件的方式來看,一切都是順理成章的,因為這裡的Foo其實是元類的物件,物件+()執行元類的__call__方法。請注意,在Foo進行例項化時候返回的物件是None,這是因為__call__方法返回的就是None,所以在沒有必要的前提下最好不要隨意重寫元類的__call__方法,這會影響到類的例項化。__call__方法在元類中作用是控制類生成時的呼叫過程。
通過__call__方法我們能得出結果就是__call__方法返回什麼,我們最後得到的例項就是什麼。還是剛才栗子,我們讓Foo例項化以後變成一個字串:
class MyType(type): def __init__(self, cls_name, bases, cls_attr): print("Mytype __init__", cls_name, bases) def __new__(cls, cls_name, bases, cls_attr): return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr) def __call__(self, *args, **kwargs): return 'this is wd' class Foo(metaclass=MyType): def __init__(self, name): print('this is __init__') self.name = name def __new__(cls, *args, **kwargs): print('this is __new__') return object.__new__(cls) def __call__(self, *args, **kwargs): print("this is __call__") obj = Foo('wd') # 例項化 print(type(obj),obj) 結果: Mytype __init__ Foo () <class 'str'> this is wd
既然__call__方法返回什麼,我們例項化生成的物件就是什麼,那麼在正常的流程是返回的是Foo的物件,而Foo的物件是由Foo的__new__和Foo的__init__生成的,所以在__call__方法的內部又有先後呼叫了Foo類的__new__方法和__init__方法,如果我們重寫元類的__call__方法,則應該呼叫物件的__new__和__init__,如下:
class MyType(type): def __init__(self, cls_name, bases, cls_attr): print("Mytype __init__", cls_name, bases) def __new__(cls, cls_name, bases, cls_attr): return super(MyType, cls).__new__(cls, cls_name, bases, cls_attr) def __call__(self, *args, **kwargs): print("Mytype __call__", ) obj = self.__new__(self) print(self, obj) self.__init__(obj, *args, **kwargs) return obj class Foo(metaclass=MyType): def __init__(self, name): self.name = name def __new__(cls, *args, **kwargs): return object.__new__(cls) obj = Foo('wd') # 例項化 print(obj.name) 結果: Mytype __init__ Foo () Mytype __call__ <class '__main__.Foo'> <__main__.Foo object at 0x1100c9dd8> wd
同樣,當函式作為元類時候,metaclass關鍵字會呼叫其對應的函式生成類,如果這個函式返回的不是類,而是其他的物件,那麼使用該函式定義的類就得到的就是該物件,這也就是為什麼我說使用函式作為元類時候,需要有type功能,一個簡單的示例:
def func(cls_name, bases, dict_attr): return 'this is wd' class Foo(metaclass=func): def __init__(self, name): self.name = name def __new__(cls, *args, **kwargs): return object.__new__(cls) print(Foo, "|", type(Foo)) # 結果:this is wd | <class 'str'> obj=Foo('wd') #報錯
結語
現在說python一切皆物件可以說非常到位了,因為它們要不是類的物件,要不就是元類的物件,除了type。再者元類本身其實是複雜的,只是我們在對這元類生成類的這一過程做了深度的分析,所以在我們編寫的程式中可能極少會用到元類,除非有特殊的需求,比如動態的生成類、修改類的一些東西等,當然你想讓你的程式碼看來“複雜”也可以嘗試使用。但是在有些情況下(如在文章中提到的幾個場景中)使用元類能更巧妙的解決很多問題,不僅如此你會發現元類在很多開源框架中也有使用,例如django、flask,你也可以借鑑其中的場景對自己的程式進行優化改進。