metaclasses可以說是Python最難掌握的範疇。如果是第一次接觸這些概念,很容易出現我是誰?,我在幹麻?,我要去哪裡?的徵狀。如果出現類似的情形,請不要驚慌,這是非常正常的XD
在日常應用中,直接使用metaclasses的機會不高,但了解metaclasses能讓我們由另一個視角,來欣賞Python的優雅。
下面引用Python大神,Tim Peters(註1),對於metaclasses的描述:
“Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).”
— Tim Peters
instance的initialize及instantiate。class的生成過程。__call__的細節。metaclasses相關知識。Python的class是繼承object而來,所以
class MyClass:
pass
是等義於
class MyClass(object):
pass
如果help(object),可以看到其內建有__init__(instance method)及__new__(static method)。
| __init__(self, /, *args, **kwargs)
| Initialize self. See help(type(self)) for accurate signature.
| Static methods defined here:
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
initialize是指instance已經建立,而我們要進一步初始化,一般為呼叫__init__。
instantiate則是指使用__new__建立instance。
__init__# 01中,建立了客製化的__init__(即overwrite了object.__init__),使得透過MyClass建立的instance於初始化時,可以指定instance variable self.x的值。
# 01
class MyClass:
def __init__(self, x):
self.x = x
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
__init__是個instance method,而instance method的第一個參數,一般約定俗成地取名為self。既然self能夠被傳入__init__,代表instance本身是在__init__被呼叫前即已建立。
__new__事實上,object.__new__才是Python真正建立instance時所呼叫的。
# 02中,建立了客製化的__new__(即overwrite了object.__new__)。
__new__的第一個參數為cls,在# 02中即為MyClass,至於其它參數則需要與__init__一致(如果有的話)。__new__中呼叫super().__new__(cls)來建立instance,在# 02中呼叫super().__new__(cls)相當於呼叫object.__new__(cls),但習慣使用super()的寫法,可以幫助我們在繼承的時候,減少一些問題(註2)。__new__回傳一個MyClass的instance時(註3),會自動呼叫__init__,我們可以id來確認__new__中的instance就是傳入__init__中的self。# 02
class MyClass:
def __new__(cls, x):
instance = super().__new__(cls)
print(f'{id(instance)=}')
return instance
def __init__(self, x):
print(f'{id(self)=}')
self.x = x
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
理論上,任何在__init__中能做的事,都能夠在__new__中完成。
# 03中,我們將__init__中指定的instance variable self.x搬到__new__中,如此則可免去建立__init__。
# 03
class MyClass:
def __new__(cls, x):
instance = super().__new__(cls)
instance.x = x
return instance
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
如果對於理解# 03有困難的朋友,或許可以將instance想為self,改寫為# 04。
# 04
class MyClass:
def __new__(cls, x):
self = super().__new__(cls)
self.x = x
return self
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.__dict__) # {'x': 1}
假設現在有個SlowNewClass,而其__new__有很多操作,需時良久(以time.sleep(1)表示)。
# 05
import time
class SlowNewClass:
def __new__(cls, **kwargs):
time.sleep(1)
return super().__new__(cls)
def __init__(self, **kwargs):
self.__dict__.update(**kwargs)
...
題目要求:
class,且必須繼承SlowNewClass。class僅接受**kwargs參數,且kwargs內所有value必須滿足value>=0,否則raise ValueError。若kwargs中有x變數,需要將其從kwargs移出,進行某些操作(以self.x = x+100表示),再將剩下的kwargs利用super().__init__往上傳遞。於# 05中,我們建立了MyClass與MyClass2,並利用timer來觀察instance的生成速度。
# 05
...
def timer(cls, **kwargs):
try:
start = time.perf_counter()
my_inst = cls(**kwargs)
except ValueError:
pass
finally:
end = time.perf_counter()
elapsed = end - start
print(f'{elapsed=:.6f} secs for {cls}')
class MyClass(SlowNewClass):
def __init__(self, **kwargs):
if all(value >= 0 for value in kwargs.values()):
if x := kwargs.pop('x', None):
self.x = x+100
super().__init__(**kwargs)
else:
raise ValueError
class MyClass2(SlowNewClass):
def __new__(cls, **kwargs):
if all(value >= 0 for value in kwargs.values()):
return super().__new__(cls, **kwargs)
raise ValueError
def __init__(self, **kwargs):
if x := kwargs.pop('x', None):
self.x = x+100
super().__init__(**kwargs)
if __name__ == '__main__':
my_inst = MyClass(x=1, y=2)
print(my_inst.__dict__) # {'x': 101, 'y': 2}
my_inst2 = MyClass2(x=1, y=2)
print(my_inst2.__dict__) # {'x': 101, 'y': 2}
print('normal: ')
timer(MyClass, x=1, y=2) # 1.000700
timer(MyClass2, x=1, y=2) # 1.000952
print('exceptions: ')
timer(MyClass, x=-1, y=2) # 1.000298
timer(MyClass2, x=-1, y=2) # 0.000011
MyClass,於__init__中進行操作。但是這有一個缺點是速度很慢,因為我們必須繼承SlowNewClass,透過其耗時的__new__來生成instance。換句話說,即使我們很快就判斷出需要raise ValueError,我們還是得等待SlowNewClass.__new__生成instance後才可操作。MyClass2同時實作__new__及__init__。這樣一來,我們可以於__new__呼叫super()__new__前,就先決定是否要raise ValueError,然後將後續需要呼叫super().__init__的工作放到__init__。MyClass與MyClass2速度差不多,但是在有例外的情況下,MyClass2可以馬上raise。由於__new__的第一個參數是cls,所以除了能指定instance variable外,也能對MyClass做一些手腳,例如插入一個instance method hi。
# 06
class MyClass:
def __new__(cls, x: int):
cls.hi = lambda self: 'hi'
instance = super().__new__(cls)
instance.x = x
return instance
if __name__ == '__main__':
my_inst = MyClass(1)
print(my_inst.hi()) # hi
但是這麼寫有點「微妙」,因為這相當於在每次生成instance時,都會mutate cls一次,建立一個新的instance method hi。
# 07嘗試繼承str,並攔截給定字串,於其後加上_123。
# 07
class MyStr1(str):
def __init__(self, s):
super().__init__(s + '_123')
class MyStr2(str):
def __new__(cls, s):
return super().__new__(cls, s + '_123')
if __name__ == '__main__':
# my_str1 = MyStr1('abc') # TypeError
my_str2 = MyStr2('abc')
print(my_str2) # 'abc_123'
MyStr1中,super().__init__會raise TypeError。MyStr2中,super().__new__,則可以成功得到abc_123。class中實作__new__?__init__之前,於建立instance前後做一些操作時(實例說明1)。__new__中操控cls,暗指每次生成instance時,都會mutate cls,需謹慎考慮這是否為您想要的行為(實例說明2)。C實作的built-in type時。註1:Tim Peters也是著名Zen of Python的作者。什麼?您沒聽過嗎?那麼請打開Python的repl,輸入import this好好欣賞一下吧。
註2:如果不太熟悉super()的朋友,可以參考這篇由Raymond Hettinger所寫的介紹。
註3:如果__new__所回傳的並非cls的instance,則不會自動呼叫__init__。但儘管如此,我們可以手動呼叫__init__,所以可以寫出如# 101的code。
# 101
from types import SimpleNamespace
class MyClass:
def __new__(cls, x):
return SimpleNamespace()
def __init__(self, x):
self.x = x
def hi(self):
return 'hi'
if __name__ == '__main__':
my_inst = MyClass(1) # __init__ not being called
print(type(my_inst)) # <class 'types.SimpleNamespace'>
MyClass.__init__(my_inst, 1)
print(my_inst.__dict__) # {'x': 1}
print(MyClass.hi(my_inst)) # hi