metaclass

May 12, 2022

type 本身既然是個類別,那麼可以繼承它嗎?可以的!

建立 meta 類別

type 類別的子類別,一樣可以指定類別名稱(字串)、類別的父類別(tuple) 與類別的屬性(dict)三個引數,例如:

class SomeMeta(type): # 繼承type類別
    def __new__(mcls, clsname, bases, attrs):
        cls = super().__new__(mcls, clsname, bases, attrs)
        print('SomeMeta __new__', mcls, clsname, bases, attrs)
        return cls

    def __init__(self, clsname, bases, attrs):
        super().__init__(clsname, bases, attrs)
        print('SomeMeta __init__', self, clsname, bases, attrs) 

Some = SomeMeta('Some', (object,), {'doSome' : (lambda self, x: print(x))})

s = Some()
s.doSome(10)

在上面的例子中,繼承 type 建立了 SomeMeta,定義了 __new____init__ 方法,__new__ 方法傳回的實例,才是 Some 最後會參考的類別,接著 __init__ 進行該類別的初始化。

執行的結果會是:

SomeMeta __new__ <class '__main__.SomeMeta'> Some (<class 'object'>,) {'doSome': <function <lambda> at 0x0000023AA0F72E50>}
SomeMeta __init__ <class '__main__.Some'> Some (<class 'object'>,) {'doSome': <function <lambda> at 0x0000023AA0F72E50>}
10 

metaclass

在上例中,直接使用 SomeMeta 建構類別實例,實際上,可以在使用 class 定義類別時,指定 metaclassSomeMeta

>>> class Other(metaclass = SomeMeta):
...     def doOther(self, x):
...         print(x)
...
SomeMeta __new__ <class '__main__.SomeMeta'> Other () {'__module__': '__main__', '__qualname__': 'Other', 'doOther': <function Other.doOther at 0x0000023AA0F8D0D0>}
SomeMeta __init__ <class '__main__.Other'> Other () {'__module__': '__main__', '__qualname__': 'Other', 'doOther': <function Other.doOther at 0x0000023AA0F8D0D0>}
>>> other = Other()
>>> other.doOther(10)
10
>>>

繼承了 type 的類別可以作為 metaclassmetaclass 是個協定,若指定了 metaclass 的類別,Python 在剖析完類別定義後,會使用指定的 metaclass 進行類別的建構與初始化,其作用就像先前的範例。

如果使用 class 定義類別時繼承某個父類別,也想指定 metaclass,可以如下:

class Other(Parent, metaclass = OtherMeta):
    pass

若想改變一個類別建立實例與初始化的流程,可以繼承 type,定義 __new____init__ 方法,作為定義類別時,metaclass 的指定對象。

一個有趣的事實是,metaclass 並不僅僅可指定 type 子類別,只要是 callable 物件都可以,這是因為建立 metaclass 的實例時,就是呼叫了 __call__,其中才呼叫了 __new____init__ 方法,然而使用靜態分析工具,像是 mypy 檢查時,可能會發生 “Invalid metaclass” 之類的錯誤。

模擬 ABC、abstractmethod

了解這些之後,就可以嘗試模仿〈共同行為與 is a〉中看過的 ABCabstractmethod 函式:

def abstract(func):
    func.__isabstract__ = True # 標示這個函式是個抽象方法
    return func

def absmths(cls, mths):
    cls.__abstractmethods__ = frozenset(mths)

class Abstract(type):
    def __new__(mcls, clsname, bases, attrs):
        cls = super().__new__(mcls, clsname, bases, attrs)

        # 類別上定義的抽象方法
        abstracts = {name for name, value in attrs.items()
                       if getattr(value, "__isabstract__", False)}

        # 從父類別中繼承下來的抽象方法
        for parent in bases:
            for name in getattr(parent, "__abstractmethods__", set()):
                value = getattr(cls, name, None)
                if getattr(value, "__isabstract__", False):
                    abstracts.add(name)

        # 指定給 __abstractmethods__
        absmths(cls, abstracts)

        return cls

class AbstractX(metaclass = Abstract):
    @abstract
    def doSome(self):
        pass

# TypeError: Can't instantiate abstract class AbstractX with abstract methods doSome
x = AbstractX()

在這個範例中,被自訂的 @abstract 標註的方法,都會被設定 __isabstract__ 屬性,而在 Abstract__new__ 中,會將具有 __isabstract__ 屬性的方法收集起來,並指定給類別的 __abstractmethods__

因此,當定義類別時,metaclass 指定了 Abstract,而且使用 @abstract 標註了抽象方法,Abstract 就不能直接用來建構實例了,子類別也必須重新定義抽象方法,才能用來建構實例。

有機會的話,建議看一下 abc.ABCMeta 的原始碼,這邊的 AbstractX 只是在簡單地模仿它,另外,abc.ABC 只是個實作抽象基礎類別的便利繼承對象,看看它的原始碼就知道了:

class ABC(metaclass=ABCMeta):
    """Helper class that provides a standard way to create an ABC using
    inheritance.
    """
    __slots__ = ()

繼承 ABC,可以讓開發者不用意識到 metaclass 的存在,不過方便歸方便,在多重繼承的情況下,若其他父類別也有指定 metaclass,建構與初始的流程可能會難以掌握,這時自行指定 metaclass 就會是必要的。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter