今天我們來歸納整理一下metaclass相關的知識。
# 01為稍後會用到的程式碼,其內含有:
MyClass的class。MyClass生成,名為my_inst的instance。# 01
class MyClass(object, metaclass=type):
pass
my_inst = MyClass()
以下是我們能想到最精簡能解釋object、type、class、instance關係的解釋,或許您也會想參考這篇stackoverflow的討論。
在Python,萬物皆是object,所以萬物都是繼承object而來(包含object自己)。所以
>>> isinstance(object, object) # True
>>> issubclass(object, object) # True
我們說my_inst是MyClass的instance。所以type(my_inst)是MyClass。
同理,藉由觀察type(MyClass)為type,可以得知MyClass是type的instance(MyClass(metaclass=type)是強烈暗示)。
那麼再往上推,藉由觀察type(type)為type,可以得知type也是type的instance。其實type is type也會是True。這就像一個circular reference。所以
>>> isinstance(type, type) # True
>>> issubclass(type, type) # True
而MyClass是繼承object而來(MyClass(object)是強烈暗示),且MyClass既然是type的instance,那麼object必定也是type的instance。所以
>>> isinstance(object, type) # True
再加上type的circular reference,所以
>>> issubclass(object, type) # True
type也是繼承object而來,再加上type的circular reference,所以
>>> isinstance(type, object) # True
>>> issubclass(type, object) # True
class都是繼承object而來,且預設metaclass為type。class關鍵字後,知道我們想生成一個class時,會先呼叫type.__prepare__,準備一個mapping,作為稍後傳給type.__new__的cls_dict,裡面會幫我們加上一些attribute(如__qualname__)。class,相當於我們要呼叫type,type本身是callable,因為其metaclass(還是type)有實作__call__。type.__call__會先呼叫type.__new__,如果回傳的是MyClass的話,則會再呼叫type.__init__。至此MyClass生成完畢。instance,相當於我們要呼叫MyClass,這會呼叫MyClass的metaclass的__call__,即type.__call__。type.__call__會呼叫MyClass.__new__,如果其回傳的是MyClass的instance,則會再呼叫MyClass.__init__。# 02中,我們講解了class與instance的生成過程,並提供數個可以生成class variable及instance variable的方法。
# 02
class MyType(type):
mcls_var_x = 'x'
def __prepare__(cls, cls_bases, **kwargs):
cls_dict = {'cls_var_a': 'a'}
return cls_dict
def __new__(mcls, cls_name, cls_bases, cls_dict, **kwargs):
cls_dict['cls_var_c'] = 'c'
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
cls.say_hello = lambda self: 'MyType works!'
return cls
def __init__(cls, cls_name, cls_bases, cls_dict, **kwargs):
cls.cls_var_d = 'd'
def __call__(cls, *args, **kwargs):
instance = super().__call__(*args, **kwargs)
instance.inst_var_c = 'c'
return instance
class MyParentClass:
cls_var_e = 'e'
def good_morning(self):
return 'Good morning!'
class MyClass(MyParentClass, metaclass=MyType):
cls_var_b = 'b'
def __new__(cls, b):
instance = super().__new__(cls)
instance.inst_var_a = 'a'
return instance
def __init__(self, b):
self.inst_var_b = b
if __name__ == '__main__':
my_inst = MyClass('b')
cls_vars = {k: v
for k, v in vars(MyClass).items()
if k.startswith('cls_')}
# {'cls_var_a': 'a', 'cls_var_b': 'b', 'cls_var_c': 'c', 'cls_var_d': 'd'}
print(cls_vars)
inst_vars = vars(my_inst)
# {'inst_var_a': 'a', 'inst_var_b': 'b', 'inst_var_c': 'c'}
print(inst_vars)
# MyType.__new__
print(my_inst.say_hello()) # MyType works!
# MyParentClass
print(my_inst.cls_var_e, MyClass.cls_var_e) # e e
print(my_inst.good_morning()) # Good morning!
# MyType
print(MyClass.mcls_var_x) # x
# print(my_inst.mcls_var_x) # AttributeError
# (<class '__main__.MyType'>, <class 'type'>, <class 'object'>)
print(type(MyClass).__mro__)
# (<class '__main__.MyClass'>, <class '__main__.MyParentClass'>, <class 'object'>)
print(MyClass.__mro__)
MyClass由MyType所生成。
MyType.__prepare__會先生成一個mapping,我們此處直接生成一個dict,並於其中建立cls_var_a。MyType會呼叫type.__call__(不是MyType.__call__),幫忙呼叫MyType.__new__(不是type.__new__,因為MyType呼叫type.__call__時,有將MyType的資訊傳給type.__call__)。MyType.__new__中,我們利用super().__new__(即type.__new__)來生成MyClass。此時的cls_dict中除了cls_var_a還有位於MyClass中的cls_var_b。我們可以選擇將想建立的attribute或function放入cls_dict中,或是於生成MyClass後再動態指定。此處我們演示將cls_var_c放入cls_dict並於生成MyClass後,再動態指定say_hello function。MyType.__new__中返回的是MyClass,所以type.__call__會幫忙呼叫MyType.__init__。於MyType.__init__中我們加入cls_var_d。my_inst需要呼叫MyClass,所以會呼叫MyType.__call__。MyType.__call__將呼叫的工作delegate給super().__call__,所以實際上幫忙我們建立instance的是type.__call__。但此處我們將MyClass的資訊藉由super().__call__傳給type.__call__,所以Python會呼叫MyClass.__new__(非type.__new__)來生成instance。MyClass.__new__中我們加入inst_var_a。由於回傳的是MyClass的instance,所以MyType.__call__會幫忙呼叫MyClass.__init__。於MyClass.__init__中我們加入inst_var_b。MyType.__call__回傳instance前加入inst_var_c。上面我們示範了cls_var_a、cls_var_b、cls_var_c、cls_var_d、inst_var_a、inst_var_b、inst_var_c以及say_hello function是如何生成的。
接下來討論兩個有趣的情況。
因為MyClass繼承MyParentClass,所以可以存取位於MyParentClass的cls_var_e及good_morning function。
如果使用MyClass.mcls_var_x可以取回'x',但若使用my_inst.mcls_var_x會raise AttributeError。
不知道諸位對這個行為是否會感到疑惑呢?如果要了解Python整個attribute lookup,可能要閱讀六翼的文章。
簡單解釋的話,是當於instance.__dict__內找不到某attribute時,會往生成其的class及其MRO的__dict__中尋找。
type(MyClass).__mro__為(MyType, type, object)
由於MyClass是MyType的instance,當於MyClass.__dict__找不到mcls_var_x時,會往MyType.__dict__尋找。由於mcls_var_x是MyType的class variable,所以返回其值'x'。
Myclass.__mro__為(MyClass, MyParentClass, object)
至於my_inst是MyClass的instance,當於myinst.__dict__找不到mcls_var_x時,會往MyClass.__dict__尋找。由於找不到,於是再往上至MyParentClass.__dict__尋找。由於還是找不到,於是再往上至object.__dict__尋找。最後由於整個MRO中都找不到mcls_var_x,只能raise AttributeError給使用者。
__init_subclass____init_subclass__是於Python3.6所添加的,其會於type.__new__生成class後才被呼叫。type.__new__會先收集有實作__set_name__的attribute,於class生成後呼叫這些attribute的__set_name__。接下來__init_subclass__會由MRO上最接近的parent class來呼叫。
__init_subclass__是一個class method,它給我們一個使用繼承來mutate class的選項。我們可以使用其添加class的attribute或function等等,一些以前必須要在type.__new__中的邏輯,可以轉移到這邊,而不需要自己客製metaclass。
最後,每當迷失在metaclass的世界時,下面這段話總是能幫到我們,希望對您也有幫助。
instance由class所生成,所以instance是class生成的instance。class由metaclass所生成,所以class是metaclass生成的instance。
將class視為一種instance,再回頭看metaclass時,會有一種撥雲見日的感覺。
__call__。