iT邦幫忙

2023 iThome 鐵人賽

DAY 17
0
Software Development

Python十翼:與未來的自己對話系列 第 17

[Day17] 五翼 - Metaclasses:Metaclasses相關整理

  • 分享至 

  • xImage
  •  

今天我們來歸納整理一下metaclass相關的知識。

# 01為稍後會用到的程式碼,其內含有:

  • 一個名為MyClassclass
  • 一個由MyClass生成,名為my_instinstance
# 01
class MyClass(object, metaclass=type):
    pass

my_inst = MyClass()

環環相扣的object與type

以下是我們能想到最精簡能解釋objecttypeclassinstance關係的解釋,或許您也會想參考這篇stackoverflow的討論

在Python,萬物皆是object,所以萬物都是繼承object而來(包含object自己)。所以

>>> isinstance(object, object)  # True
>>> issubclass(object, object)  # True

我們說my_instMyClassinstance。所以type(my_inst)MyClass

同理,藉由觀察type(MyClass)type,可以得知MyClasstypeinstanceMyClass(metaclass=type)是強烈暗示)。

那麼再往上推,藉由觀察type(type)type,可以得知type也是typeinstance。其實type is type也會是True。這就像一個circular reference。所以

>>> isinstance(type, type)  # True
>>> issubclass(type, type)  # True

MyClass是繼承object而來(MyClass(object)是強烈暗示),且MyClass既然是typeinstance,那麼object必定也是typeinstance。所以

>>> isinstance(object, type)  # True

再加上typecircular reference,所以

>>> issubclass(object, type)  # True

type也是繼承object而來,再加上typecircular reference,所以

>>> isinstance(type, object)  # True
>>> issubclass(type, object)  # True

class creation

  • 所有class都是繼承object而來,且預設metaclasstype
  • 當Python看到class關鍵字後,知道我們想生成一個class時,會先呼叫type.__prepare__,準備一個mapping,作為稍後傳給type.__new__cls_dict,裡面會幫我們加上一些attribute(如__qualname__)。
  • 生成一個class,相當於我們要呼叫typetype本身是callable,因為其metaclass(還是type)有實作__call__type.__call__會先呼叫type.__new__,如果回傳的是MyClass的話,則會再呼叫type.__init__。至此MyClass生成完畢。

instance creation

  • 生成一個instance,相當於我們要呼叫MyClass,這會呼叫MyClassmetaclass__call__,即type.__call__type.__call__會呼叫MyClass.__new__,如果其回傳的是MyClassinstance,則會再呼叫MyClass.__init__

實例說明

# 02中,我們講解了classinstance的生成過程,並提供數個可以生成class variableinstance 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

MyClassMyType所生成。

  • 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。我們可以選擇將想建立的attributefunction放入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

  • 生成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。由於回傳的是MyClassinstance,所以MyType.__call__會幫忙呼叫MyClass.__init__。於MyClass.__init__中我們加入inst_var_b
  • 最後,我們於MyType.__call__回傳instance前加入inst_var_c

存取其它attribute

上面我們示範了cls_var_acls_var_bcls_var_ccls_var_dinst_var_ainst_var_binst_var_c以及say_hello function是如何生成的。

接下來討論兩個有趣的情況。

MyParentClass

因為MyClass繼承MyParentClass,所以可以存取位於MyParentClasscls_var_egood_morning function

MyType

如果使用MyClass.mcls_var_x可以取回'x',但若使用my_inst.mcls_var_xraise AttributeError

不知道諸位對這個行為是否會感到疑惑呢?如果要了解Python整個attribute lookup,可能要閱讀六翼的文章。

簡單解釋的話,是當於instance.__dict__內找不到某attribute時,會往生成其的class及其MRO__dict__中尋找。

type(MyClass).__mro__(MyType, type, object)

由於MyClassMyTypeinstance,當於MyClass.__dict__找不到mcls_var_x時,會往MyType.__dict__尋找。由於mcls_var_xMyTypeclass variable,所以返回其值'x'

Myclass.__mro__(MyClass, MyParentClass, object)

至於my_instMyClassinstance,當於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的選項。我們可以使用其添加classattributefunction等等,一些以前必須要在type.__new__中的邏輯,可以轉移到這邊,而不需要自己客製metaclass

小結

最後,每當迷失在metaclass的世界時,下面這段話總是能幫到我們,希望對您也有幫助。

instanceclass所生成,所以instanceclass生成的instance
classmetaclass所生成,所以classmetaclass生成的instance

class視為一種instance,再回頭看metaclass時,會有一種撥雲見日的感覺。

參考資料

  1. Python Morsels - Everything is an object
  2. Real Python - metaclasses
  3. Python 3:Deep Dive part4-155 - Classes, Metaclasses, and __call__

Code

本日程式碼傳送門


上一篇
[Day16] 五翼 - Metaclasses:__call__
下一篇
[Day18] 六翼 - 導讀Descriptor HowTo Guide:How dot works?
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言