《流暢的Python》筆記。
本篇主要討論Python中的元類。Python中所有的類都直接或間接地是元類type的例項。閱讀本篇時,請時刻注意你所閱讀的內容指的是**"例項"(或者說"物件")、"類"還是"元類"**,否則很容易被繞暈。
1. 前言
Python中,幾乎所有的東西都是物件。不光類的例項是物件,連類本身也是物件。
不像C++、Java等靜態語言,在編譯前就必須將類定義好,執行時不能再建立新類,Python則可以在執行時動態建立新類,且不通過關鍵字class
。建立類的類叫做元類。元類也是類,它可以派生出新的元類,但所有元類最頂層的超類只有一個,就是我們經常用到的type
。Python中所有的類都直接或間接地是type
的例項。
在執行時能通過元類動態建立類是Python的魅力,但想要理解這個"魅力"確並不是那麼容易。本篇內容主要有:元類的簡單示例,類裝飾器,元類的原理、定義及使用方式,最後使用元類來彌補上一篇中描述符的不足。
本篇只能算是對元類的初步介紹,更深層次的內容還需進一步學習。
2. 初識元類
通常,如果要建立物件,需要先在某個模組中用class
關鍵字定義好類,再在業務程式碼中建立這個類的例項。與這種事先定義的方式相反,可以通過type
在執行時建立類,以下是它的示例:
>>> a = "this is a string"
>>> type(a)
<class 'str'>
>>> MyClass = type("MyClass", (object,), {"x": 1, "x2": lambda self: self.x * 2})
>>> mc = MyClass()
>>> mc.x
1
>>> mc.x2()
2 # 請留意下方這三個特殊屬性
>>> mc.__class__ # __class__的值是例項所屬的類
<class 'MyClass'>
>>> MyClass.__bases__ # __bases__的值是類的所有直接超類
(<class 'object'>,)
>>> MyClass.__mro__ # __mro__的值是類的所有超類
(<class 'MyClass'>, <class 'object'>)
>>> MyClass.__class__ # 這表明MyClass這個類是type的物件
<class 'type'>
複製程式碼
上述MyClass
的定義等同於如下定義:
class MyClass(object):
x = 1
def x2(self):
return self.x * 2
複製程式碼
type
通常被當做函式使用,但它其實是一個類。當只傳入一個例項時,它返回例項的型別;當傳入3個引數時,它生成並返回一個類:
type(cls_name, bases, attr_dict)
複製程式碼
其中:
cls_name
是要建立的類的名稱的字串;bases
是一個元組,它儲存即將建立的類的直接父類們,比如MyClass
繼承自object
(如果只繼承自object
,可以將bases
設定為空元組);attr_dict
是新類的屬性字典。不光包括資料屬性,還包括了方法(含特殊方法)。不過,如果是資料屬性,這些資料屬性將成為類屬性,而不是例項屬性。如果想建立例項屬性,請在attr_dict
中傳入__init__
的定義,或者傳入__dict__
。
為了更詳細的介紹type
的用法,我們用它來構造一個類工廠函式。
3. 類工廠函式
對於資料結構固定的資料,如果想將其包裝成類物件,傳統的做法是使用class
定義每個類,比如為寵物應用定義各種動物類:
class Dog:
def __init__(self, name, weight, owner):
self.name = name
self.weight = weight
self.owner = owner
複製程式碼
不知道各位在敲這段程式碼時有沒有抱怨:name
,weight
,owner
敲了三遍!如果再多幾種動物類,這種樣板程式碼得寫多少?當然,對於相關的類可以選擇繼承。但如果資料間不相關呢?難道定義每個類的時候都將這種樣板程式碼敲一遍?這個時候就可以用類工廠函式來減少這種樣板程式碼。下方程式碼展示了type
更具體的用法,生成的類比較簡單,適合用於處理格式固定的資料。這個工廠函式其實是在模仿collections.namedtuple
:
def record_factory(cls_name, field_name):
try: # 假設傳入的field_name是字串,獲取屬性名
field_names = field_name.replace(",", " ").split()
except AttributeError:
pass # 如果不是字串,則當做可迭代物件處理
field_names = tuple(field_names) # 將屬性名存到元組中
# __init__不作用於這個工廠函式!這是為要建立的類定義的構造方法
def __init__(self, *args, **kwargs):
attrs = dict(zip(self.__slots__, args))
attrs.update(kwargs)
for name, value in attrs.items():
setattr(self, name, value)
def __iter__(self): # 讓即將建立的類可迭代
for name in self.__slots__:
yield getattr(self, name)
def __repr__(self): # 格式化輸出
values = ", ".join("{}={!r}".format(*i) for i in zip(self.__slots__, self))
return "{}({})".format(self.__class__.__name__, values)
# 類將擁有的屬性
cls_attrs = dict(__slots__=field_names, __init__=__init__,
__iter__=__iter__, __repr__=__repr__)
return type(cls_name, (), cls_attrs) # 繼承自object
複製程式碼
下面是這個類工廠函式的用法:
>>> Dog = record_factory("Dog", "name weight owner")
>>> dog = Dog("test", 5, "Kevin")
>>> dog
Dog(name='test', weight=5, owner='Kevin')
>>> dog.weight = 6
>>> dog
Dog(name='test', weight=6, owner='Kevin')
>>> name, weight, owner = dog
>>> name, weight, owner
('test', 6, 'Kevin')
複製程式碼
下面我們將進一步瞭解元類。
4. 元類
物件導向的思想有兩大關係:類的繼承和類的例項化。在Python中,type
和object
就像兩個緊密合作的管理員,type
主管例項化,object
主管繼承。
我們都知道,Python中所有的類都是從object
繼承而來的。但如果你看過下方的程式碼後,不知道對這一點的理解會不會動搖:
>>> type.__bases__
(<class 'object'>,)
>>> type.__class__
<class 'type'>
>>> object.__bases__
()
>>> object.__class__
<class 'type'>
複製程式碼
這段程式碼翻譯成中文就是:object
是type
的例項,type
是object
的子類,因此type
也是type
自身的例項。這裡完美地扯出了一個"先有蛋還是先有雞"的問題:既然object
是type
的例項,那就得先由type
建立object
;但type
又是object
的子類,也就是得先有object
,再有type
,所以到底是誰先有?
這個關係直到現在我也沒搞清楚。如果是現實世界,可以說人類暫時還沒搞清楚是先有雞還是先有蛋,但Python這種程式語言可是人造的東西,況且底層還是C語言,所以我肯定不信什麼"互為祖先,同時產生"這種說法,而且這在程式碼裡不是死迴圈嗎?查了很多資料,基本都是引用的這張圖,其中虛線表示例項關係,實線表示繼承關係:
但大家都回避了前面那個問題:object
從type
例項化而來,可type
又從object
派生而來,到底誰先存在?
只能去看原始碼。原始碼中type
確實繼承自object
,但object
的定義中並沒有metaclass
關鍵字出現(後面會講到,如果以class
的形式從元類例項化類,需要使用這個關鍵字);並且,object
中有這麼一個定義:
__class__ = None # (!) forward: type, real value is ''
複製程式碼
這就讓疑惑更深了:object
究竟是不是type
的例項?type
中有如下定義:
__bases__ = (
object,
)
__base__ = object
__mro__ = (
None, # (!) forward: type, real value is ''
object,
)
複製程式碼
更深層的原始碼暫時還啃不動。官方說明文件中說明了類的構建過程:所有的類,不管指沒指明元類,都會經由type()
,產生實際使用的類,這也驗證了所有的類都是type
的例項這一說法。
這兩者的具體關係還有待繼續研究。但我們畢竟不是語言專家,我們更看重的是元類怎麼使用。對於這些概念,我們只需要知道:
- 元類
type
可以建立類; - 所有的類都直接或間接的是
type
的例項; type
是自身的例項;type
可以被繼承,用於建立新的元類。
4.1 類裝飾器
在繼續元類之前,我們先來解決上一篇屬性描述符沒有解決的問題:儲存屬性需要手動指定,而自動生成的名稱所表達的意思又不夠明顯:
>>> Food.weight.storage_name
'_Quantity#0'
複製程式碼
這是上一篇文章中自動生成儲存屬性的名稱時採用的策略,但我們更希望是下面這種形式:
>>> Food.weight.storage_name
'_Quantity#weight'
複製程式碼
上一篇中也說過,描述符類很難獲取託管類的類屬性名稱的。使用類裝飾器則能解決這個問題。類裝飾器和函式裝飾器非常相似,是引數為類物件的函式,返回原來的類或修改後的類。這裡我們將它裝飾到Food
類上,而不是Quantity
類上(Food
和Quantity
的具體定義請檢視上一篇文章。以下程式碼不能直接執行,請自行匯入所需的類):
@entity
class Food: # 這個類比上一篇有所省略
weight = Quantity() # 並沒有傳入儲存屬性的名稱
def __init__(self, weight):
self.weight = weight
def entity(cls):
for key, attr in cls.__dict__.items():
if isinstance(attr, Validated): # 如果這個屬性是Validated類的例項
type_name = type(attr).__name__ # 則修改它的storage_name屬性的值
attr.storage_name = "_{}#{}".format(type_name, key)
return cls
複製程式碼
其實實現的思路很簡單:Quantity
之所以無法獲取Food
類的屬性名,是因為在Food
中生成Quantity
例項時,Food
這個類都還沒有建立完畢,自然只能手動傳入。那等Food
類建立完畢了再設定值不就行了?與函式裝飾器類似,類裝飾器會在Food
生成後立即執行。
也可以用類裝飾器來替換掉類中的某些方法。但類裝飾器有一個重大缺點:只能對直接依附的類有效。這意味著,被裝飾類的子類不一定繼承裝飾器所做的修改,具體情況視改動的方式而定。
小插曲:我看到這個概念的時候,無法理解為什麼這被稱之為"缺點":繼承的時候子類重寫了父類的同名方法,這不再正常不過嗎?難道是不準讓子類重寫這個方法,要讓這個方法在整個繼承體系中都保持唯一?那不重寫不就完了嗎?如果整個專案就一個人做,當然能保證不重寫這個方法。但往往軟體開發是個團隊專案,其他人並不一定清楚這個方法能不能被重寫。
要保持方法在整個繼承體系中保持唯一,不被子類所覆蓋,這就得祭出元類。
4.2 使用元類
當使用到元類的時候,其實就是在定製化類及其子類的行為。下面我們使用元類替換掉前面的類裝飾器:
# Validated和Quantity都在上一篇文章中,以下程式碼不能直接執行!
class EntityMeta(type): # 在元類中,通常將self換成cls
def __init__(cls, name, bases, attr_dict): # 邏輯和類裝飾器是一樣的
super().__init__(name, bases, attr_dict)# 這一步將name,bases,attr_dict繫結到了cls上
for key, attr in attr_dict.items():
if isinstance(attr, Validated):
type_name = type(attr).__name__
attr.storage_name = "_{}#{}".format(type_name, key)
class Entity(metaclass=EntityMeta):
"""帶有驗證欄位的業務實體""" # 什麼都不用做,除非像新增新方法
class Food(Entity): # 對這個類進行了簡化
weight = Quantity()
def __init__(self, weight):
self.weight = weight
複製程式碼
請注意區分這些類的關係:
EntityMeta
是元類type
的子類,所以它也是個元類。Entity
使用class
關鍵字來定義新的類,而不是呼叫type()
函式來建立新的類;在定義Entity
時,使用了metaclass
關鍵字,表明這是元類EntityMeta
的例項,而不是EntityMeta
的子類,即,這不是繼承關係。同時,Entity
也(間接)是type
的例項。Food
是Entity
的子類,也是元類EntityMeta
和type
的例項。
列出這些關係,是想提醒大家,如果要自行定義元類,請時刻注意,究竟誰是誰的子類,誰是誰的例項。下面來執行一下這段程式碼:
>>> Food.weight.storage_name
'_Quantity#weight' # 行為符合預期
複製程式碼
這裡又產生了3個問題。
第1個問題是:從EntityMeta
的__init__
中可以看到,引數cls
存的是元類的例項的引用,即類Entity
或者類Food
的引用,但整個初始化過程中,根本就沒有用到cls
,可結果表明,這些修改確實作用到了Food
上,那麼這是怎麼作用Food
上的呢?
EntityMeta.__init__()
這個方法中的語句並不多,簡答分析就能知道,問題出在super().__init__()
,即type.__init__()
上。但這個方法的具體實現我暫時也不知道,只能給出我的猜想:我們都知道,對於普通的類(例如Food
)來說,它的物件(例如f
)儲存在記憶體中的某個位置a
上,Food
的__init__
操作記憶體a
上的資料;而開篇就提到,所有的類也都是物件,於是類比一下,元類(例如EntityMeta
)的例項(例如Food
)肯定也儲存在記憶體的某個位置b
,那麼type.__init__
肯定將傳入的引數關聯到了記憶體b
(例項Food
)上。所以,這些操作最後在Food
上生效了。平時對類的使用,其實就是用記憶體b
中的資料建立記憶體a
中的資料。
元類之所以難理解,難就難在我們並不習慣於"根據類來建立類"這種思路,我們習慣的是"根據類來建立例項"。這裡也再次申明,沒有"根據類來建立類"這種說法,一切都是"根據類來建立例項"!當涉及到元類時,就把元類看做平常使用的類,把元類生成的類看做平常使用的例項(或者說物件)。如果能這樣想,下面兩個問題也就能很好回答:
- 兩個
__init__
誰先執行呢? - 前面說到,在元類中定義的方法能影響到整個繼承體系,即使子類重寫這個方法也沒有用,那這是怎麼做到的呢?
要徹底回答這兩個問題,就要涉及到執行時與匯入時的概念。
5. 執行時&匯入時
為了正確地做超程式設計,必須知道Python直譯器在什麼時候執行各個程式碼塊。Python程式設計師區分執行時與匯入時這兩個概念,但其實這兩個術語並沒有嚴格定義,而且兩者有交集。
在匯入時,直譯器會編譯原始檔,直譯器會從上到下一次性解析完整個.py
模組,然後生成用於執行的位元組碼,並將其存到.pyc
檔案中(這類檔案在本地的__pycache__
資料夾中)。所以,雖然Python是解釋型語言,邊解釋邊執行,但解釋的不是.py
原始檔,而是.pyc
中的位元組碼資料。
匯入時除了編譯,還會做些其他的事情。由於Python中的語句幾乎都是可執行的,稍不注意,某些本該在執行時才執行的語句會在匯入時就執行,導致使用者程式的狀態被修改。這裡指的就是import
語句:
- 在Java中,
import
只用作宣告,執行的時候才真正執行import
後面的包中的程式碼。 - 在Python中,
import
不僅僅是宣告。模組首次被匯入時,模組中所有的程式碼都會被執行並快取,以後再匯入相同的模組時直接使用快取(只做名稱繫結);所匯入的模組中如果還有import
,那麼這些模組只要沒被匯入過,也會被執行一遍。這表明,執行時與匯入時產生了交集。
之前我在網上搜尋這個概念的時候,很多博主都說,匯入時會執行模組中所有的程式碼。其實並不是這樣的,Python直譯器確實會將模組從頭執行到尾,但是:
- 對於函式,直譯器只執行完
def
關鍵字所在的行,它會編譯函式的定義體,把函式物件繫結到對應的全域性名稱上,但顯然函式定義體不會被執行。只有到執行時,直譯器才通過全域性名稱找到函式定義體,再執行它。 - 對於類,情況就不一樣了。匯入時,直譯器會執行每個類的定義體,甚至會執行巢狀類的定義體。這麼做的結果就是,定義了類的屬性和方法(方法的定義體依然不會被執行),並構建了類這個物件。
繞了這麼一大圈,終於和元類發生了關係!類這個物件在匯入時就會被建立,所以,元類在匯入時就會初始化它的例項:類。
為了更真切的體驗這個過程,下面建立幾個類,並觀察直譯器在匯入時和執行時的行為。
下面的程式碼會用到類裝飾器和元類,前面說到類裝飾器在子類中不一定起作用,但元類一定起作用,請留意這兩個的行為。
5.1 一般情況
這裡指沒有元類的情況。首先建立兩個模組,程式碼可能有點長,但都很簡單。注意這兩個模組的名稱,首先是evaltime.py
:
# evaltime.py
from evalsupport import deco_alpha
print('<[1]> evaltime module start')
class ClassOne: # 它巢狀了一個類
print('<[2]> ClassOne body')
def __init__(self):
print('<[3]> ClassOne.__init__')
def __del__(self):
print('<[4]> ClassOne.__del__')
def method_x(self):
print('<[5]> ClassOne.method_x')
class ClassTwo(object):
print('<[6]> ClassTwo body')
@deco_alpha
class ClassThree: # 它被類裝飾器裝飾
print('<[7]> ClassThree body')
def method_y(self): # 注意才場景2中觀察這個方法的行為
print('<[8]> ClassThree.method_y')
class ClassFour(ClassThree): # 這裡有一個繼承,ClassThree被類裝飾器裝飾過
print('<[9]> ClassFour body')
def method_y(self): # 注意才場景2中觀察這個方法的行為
print('<[10]> ClassFour.method_y')
if __name__ == '__main__':
print('<[11]> ClassOne tests', 30 * '.')
one = ClassOne()
one.method_x()
print('<[12]> ClassThree tests', 30 * '.')
three = ClassThree()
three.method_y()
print('<[13]> ClassFour tests', 30 * '.')
four = ClassFour()
four.method_y()
print('<[14]> evaltime module end')
複製程式碼
接著是evalsupport.py
:
# evalsupport.py
print('<[100]> evalsupport module start')
def deco_alpha(cls):
print('<[200]> deco_alpha')
def inner_1(self):
print('<[300]> deco_alpha:inner_1')
cls.method_y = inner_1
return cls
class MetaAleph(type):
print('<[400]> MetaAleph body')
def __init__(cls, name, bases, dic):
print('<[500]> MetaAleph.__init__')
def inner_2(self):
print('<[600]> MetaAleph.__init__:inner_2')
cls.method_z = inner_2 # 例項中的這個屬性如果有,則會被替換
# 如果沒有,則新建這個屬性並賦值為內嵌函式inner_2的引用
print('<[700]> evalsupport module end')
複製程式碼
上面這兩個模組的程式碼中有<[N]>
標記,N
表示數字。現在請大家模擬以下兩種場景,記錄標記出現的順序,最後再和真實結果比較。
場景1:在Python控制檯中以互動的方式匯入evaltime.py
模組,即
>>> import evaltime.py
複製程式碼
場景2:在命令列中執行evaltime.py
模組,即
$ python3 evaltime.py
複製程式碼
建議模擬完後再看下面的結果:
# 場景1
>>> import evaltime.py
<[100]> evalsupport module start # 執行evalsupport.py模組
<[400]> MetaAleph body # MetaAleph的定義體執行了
<[700]> evalsupport module end # 函式deco_alpha定義體在匯入時並沒有被執行!
<[1]> evaltime module start # 開始執行evaltime.py模組
<[2]> ClassOne body # ClassOne的定義體被執行了,但其中的方法沒有被執行
<[6]> ClassTwo body # 巢狀的ClassTwo的定義體也被執行了
<[7]> ClassThree body # ClassThree的定義體被執行。
<[200]> deco_alpha # 跳到了類裝飾器中,函式定義體在匯入時被執行了!證明匯入時建立了類物件
<[9]> ClassFour body # 類定義體被執行
<[14]> evaltime module end # 模組執行完畢
# 場景2
$ python3 evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body # 在此行之前,和匯入時沒有區別,畢竟要執行得先匯入嘛
<[11]> ClassOne tests .............................. # 開始執行if中的內容了
<[3]> ClassOne.__init__ # 初始化ClassOne
<[5]> ClassOne.method_x # 呼叫ClassOne的method_x方法
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1 # ClassThree的method_y被替換了
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y # 類裝飾器在子類上不起作用
<[14]> evaltime module end # 模組執行結束
<[4]> ClassOne.__del__ # ClassOne在被銷燬時呼叫__del__方法
複製程式碼
不知大家的模擬是否和結果一致?
場景2中的結果證明了,類裝飾器在子類中不一定起作用。
兩個場景中,類裝飾器在匯入時都執行了一次,這證明了類物件在匯入時建立,而不是在執行時建立。
5.2 加入元類
還剩下的兩個問題將在這個例子中找到答案。不過,還得再建立一個模組evaltime_meta.py
,並建議大家回顧一下MetaAleph
的實現:
# evaltime_meta.py
from evalsupport import deco_alpha, MetaAleph
print('<[1]> evaltime_meta module start')
@deco_alpha
class ClassThree(): # 被類裝飾器裝飾
print('<[2]> ClassThree body')
def method_y(self):
print('<[3]> ClassThree.method_y')
class ClassFour(ClassThree):
print('<[4]> ClassFour body')
def method_y(self):
print('<[5]> ClassFour.method_y')
class ClassFive(metaclass=MetaAleph): # 它是元類MetaAleph的例項!
print('<[6]> ClassFive body')
def __init__(self):
print('<[7]> ClassFive.__init__')
def method_z(self):
print('<[8]> ClassFive.method_y')
class ClassSix(ClassFive): # 它也是元類MetaAleph的例項!
print('<[9]> ClassSix body')
def method_z(self):
print('<[10]> ClassSix.method_y')
if __name__ == '__main__':
print('<[11]> ClassThree tests', 30 * '.')
three = ClassThree()
three.method_y()
print('<[12]> ClassFour tests', 30 * '.')
four = ClassFour()
four.method_y()
print('<[13]> ClassFive tests', 30 * '.')
five = ClassFive()
five.method_z()
print('<[14]> ClassSix tests', 30 * '.')
six = ClassSix()
six.method_z()
print('<[15]> evaltime_meta module end')
複製程式碼
還是那兩個場景:
場景1:在Python控制檯中匯入evaltime_meta.py
>>> import evaltime_meta.py
複製程式碼
場景2:在命令列中執行evaltime_meta.py
$ python3 evaltime_meta.py
複製程式碼
以下是兩個場景的結果:
# 場景1
>>> import evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime_meta module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body # 到這裡為止,和上一個場景1的情況一樣
<[6]> ClassFive body # 執行了ClassFive定義體
<[500]> MetaAleph.__init__ # 元類中的初始化方法在匯入時被執行了!也證明匯入時建立了類物件
<[9]> ClassSix body
<[500]> MetaAleph.__init__ # 再次觸發元類中的初始化方法,
<[15]> evaltime_meta module end
# 場景2
$ python3 evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime_meta module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__
<[9]> ClassSix body
<[500]> MetaAleph.__init__ # 到此行位置,和場景1的情況一樣
<[11]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1 # 方法被類裝飾器替換
<[12]> ClassFour tests ..............................
<[5]> ClassFour.method_y # 類裝飾器對子類不起作用
<[13]> ClassFive tests ..............................
<[7]> ClassFive.__init__ # 初始化ClassFive的例項five
<[600]> MetaAleph.__init__:inner_2 # 方法被替換
<[14]> ClassSix tests ..............................
<[7]> ClassFive.__init__ # 初始化ClassFive的子類ClassSix的例項six
<[600]> MetaAleph.__init__:inner_2 # 子類的方法也被替換了!
<[15]> evaltime_meta module end
複製程式碼
這組例子再一次證明了類物件在匯入時建立!並且元類對它的類物件的初始化也在匯入時進行。其實,匯入時對於元類來說就是它的執行時。
現在來回答之前留下的兩個問題:
- 元類只要有例項,元類的
__init__
方法就一定先於例項的__init__
方法先執行。比較這兩者的__init__
方法有些牽強,畢竟類物件(例如ClassFive
)在執行時建立,因此元類的__init__
方法必定在匯入時執行;而類例項在執行時才建立,類物件的__init__
方法也就只能在執行時才執行。其實就是一個顯而易見的邏輯:什麼時候建立"例項",就什麼時候執行"類"中的__init__
方法咯。不過得清楚,這裡的"例項"和"類"究竟指代的是誰。 - 上一條解釋其實已經回答了"元類為什麼能覆蓋所有子類的方法"。
ClassFive
是元類MetaAleph
的例項,而不是繼承自MetaAleph
;ClassSix
雖繼承自ClassFive
,但又不是繼承自MetaAleph
,它僅僅只是MetaAleph
的又一個例項而已。這裡說的覆蓋對元類而言根本就不是覆蓋,元類僅僅只是在為它的例項的屬性賦值而已:你(ClassSix
)只是我(MetaAleph
)的例項,你繼承自我的另一個例項(ClassFive
),又不是繼承自我,所以你跟我談什麼繼承與覆蓋?我只是在給你的屬性賦值而已!
本文對元類的介紹到此結束。這些內容僅僅只是元類的皮毛。其中有很多地方依然沒有弄懂,繼續努力吧!
5.3 補充
其實如果想彌補本文中類裝飾器的缺陷,可以不用定義元類,現在有更方便的方法:定義特殊方法__init_subclass__
。它的作用和本文中的元類一樣,但比建立元類簡單直觀得多。在建立子類時,子類都會被這個方法初始化。
6. 總結
本文首先展示了元類的基本用法:直接用type()
函式建立類,然後將元類用到了類工廠函式中。之後加入了一個小插曲,類裝飾器;接著深入介紹了元類的概念性知識,並展示瞭如何使用class
和metaclass
關鍵字從元類建立類。最後,介紹了執行時與匯入時的概念,並通過程式碼展示了這兩者的區別,尤其展示了類的建立時間。
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~