metaclass
May 12, 2022type
本身既然是個類別,那麼可以繼承它嗎?可以的!
建立 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
定義類別時,指定 metaclass
為 SomeMeta
:
>>> 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
的類別可以作為 metaclass
,metaclass
是個協定,若指定了 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〉中看過的 ABC
及 abstractmethod
函式:
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
就會是必要的。