今天我們分享class是如何生成的,其實關鍵都在type這個built-in。
typetype有兩種常用的使用情況:
type。class,是一種動態生成class的方式。type的signature如下:
type(cls_name, cls_bases, cls_dict)
cls_name為想建立class的名字。cls_bases為新建立class的MRO上所有class,為一tuple。如果給予一個空的tuple,Python會自動將object加進去,即(object, )。cls_dict含有想建立class的所有資訊。type建立class# 01
class MyClass:
def __init__(self, x):
self.x = x
# 01是一個基本的class,我們可以將其拆為幾個部份,分別對應利用type建立class的過程:
class是能夠建立class這種型態的關鍵字,在這裡我們想用type取代。cls_name為'MyClass'。MyClass為繼承object而來,即cls_bases為(object, )或以空tuple代替。MyClass內含有function或attribute的部份為cls_body。其實是一堆字串,我們需要一個方法來轉化這些字串,將轉換後的狀態記錄於cls_dict,使其能為type所用,而內建的exec正好可以派上用場。# 02模擬利用type建立class。
# 02
cls_dict = {}
cls_body = '''
def __init__(self, x):
self.x = x
'''
exec(cls_body, globals(), cls_dict) # populating cls_body into cls_dict
cls_name = 'MyClass'
cls_bases = ()
MyClass = type(cls_name, cls_bases, cls_dict)
MyType建立class既然type是一種class,表示我們可以繼承type建立一個新的MyType。這麼一來,我們就可以overwrite type.__new__,在建立新class的前後動點手腳,最後再用類似前面type建立class的語法,使用MyType來建立class。
# 03
class MyType(type):
def __new__(mcls, cls_name, _cls_bases, cls_dict):
cls = super().__new__(mcls, cls_name, _cls_bases, cls_dict)
return cls
... # 中間過程如# 02
MyClass = MyType(cls_name, cls_bases, cls_dict)
print(type(MyClass)) # <class '__main__.MyType'>
class關鍵字搭配MyType作為metaclass可以看出使用class關鍵字來建立class,比使用MyType來的簡潔方便。但使用MyType的好處,是可以在建立class時動點手腳。那麼有沒有一種方法是既可以使用class 關鍵字來建立class,又同時具備我們在MyType中所做的操作呢?其實有的,而且我們一直隱性在用。
# 04中的MyClass1、MyClass2及MyClass3是等義的。class是繼承object,並由type所生成。
# 04
class MyClass1:
def __init__(self, x):
self.x = x
class MyClass2(object):
def __init__(self, x):
self.x = x
class MyClass3(object, metaclass=type):
def __init__(self, x):
self.x = x
所以我們只要將metaclass換為MyType,就可以繼續使用方便的class 關鍵字來建立class並同時具備於MyType中的操作。
# 04
...
class MyType(type):
def __new__(mcls, cls_name, cls_bases, cls_dict):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
return cls
class MyClass(object, metaclass=MyType):
def __init__(self, x):
self.x = x
__prepare__本日內容至此,不知道您有沒有疑惑過下面這個問題。
當我們利用MyType來建立class,其cls_dict是我們給予的。但是當利用class關鍵字搭配MyType作為metaclass這種建立class的方式時,其中的cls_dict是哪裡來的呢?
其實此cls_dict會由__prepare__提供。如果help(type),可以看出__prepare__為class method。
| Class methods defined here:
|
| __prepare__(...)
| __prepare__() -> dict
| used to create the namespace for the class statement
一般來說,__prepare__會返回dict(但事實上可以返回一個mapping),接著傳遞給__new__作為其cls_dict參數(註1)。然後我們可於__new__中對cls_dict進行一些操作。最後當我們使用cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)時,會將cls_dict中的東西複製到一個新的dict中(註2)。
# 05
class MyType(type):
@classmethod
def __prepare__(cls, cls_name, cls_bases, **kwargs):
print('MyType __prepare__ called')
cls_dict = {}
print(f'{id(cls_dict)=}')
return cls_dict
def __new__(mcls, cls_name, cls_bases, cls_dict):
print('MyType __new__ called')
print(f'{id(cls_dict)=}')
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
return cls
class MyClass(metaclass=MyType):
pass
MyType __prepare__ called
id(cls_dict)=2079093125440
MyType __new__ called
id(cls_dict)=2079093125440
由# 05中我們可以看出,__prepare__先於__new__被呼叫,且可以確認__prepare__中返回的cls_dict與__new__中的cls_dict是同一個obj。
Python3.7之前,一個有趣的運用是回傳一個collections.OrderedDict,來保存key-value的插入順序。但是新版本的dict已經是有序了,所以除非要維護舊版本的code,__prepare__漸漸失去了它的應用價值。有興趣深研的朋友可以參考Python參考文件及PEP 3115。
註1:於__prepare__傳到__new__的過程中,Python會幫我們加上一些attribute,如__qualname__等在此dict中。
註2:cls_dict並非新生成cls的__dict__。事實上cls.__dict__也僅為一mappingproxy。