iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
Software Development

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

[Day14] 五翼 - Metaclasses:Instance Creation

  • 分享至 

  • xImage
  •  

五翼大綱

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

  • [Day14]分享instanceinitializeinstantiate
  • [Day15]分享class的生成過程。
  • [Day16]分享使用__call__的細節。
  • [Day17]整理metaclasses相關知識。

initialize vs instantiate

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__回傳一個MyClassinstance時(註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}

實例說明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中,我們建立了MyClassMyClass2,並利用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__
  • 在正常情況下MyClassMyClass2速度差不多,但是在有例外的情況下,MyClass2可以馬上raise

實例說明2

由於__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

實例說明3

# 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__所回傳的並非clsinstance,則不會自動呼叫__init__。但儘管如此,我們可以手動呼叫__init__,所以可以寫出如# 101code

# 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

Code

本日程式碼傳送門


上一篇
[Day13] 四翼 - Descriptor:property vs Descriptor
下一篇
[Day15] 五翼 - Metaclasses:Class Creation
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言