Python 細說 type() 與 Metaclass

Posted on  Oct 26, 2023  in  Python 程式設計 - 高階  by  Amo Chen  ‐ 4 min read

Python 的類別定義有個神奇的參數,稱為 metaclass , 範例如下:

class Meta(type):
    pass

class A(metaclass=Meta):
    pass

這個參數不是繼承(inheritance), 但是它到底具體是做什麼的呢?就讓本文娓娓道來吧!

本文環境

  • Python 3

type 是什麼?

我們都知道 Python 世界裡的所有事物都是物件(objects)

而物件是用類別進行描述,告訴 Python 如何產生某物件,例如下列程式即是告訴 Python 要產生 1 個稱為 A 的物件:

>>> class A: pass
...
>>> print(A())
<__main__.A object at 0x10114ecd0>

print(A()) 的結果可以看到, A() 類別實例化之後就是個物件(object) 。

物件一定會相對應的型別(type)

在 Python 的世界裡,可以用 type() 函式查看型別。

我們可以在 Python 直譯器中輸入下列內容,用 type() 查看字串與整數的型別(type),可以看到它們的型別:

>>> type('string')
<class 'str'>
>>> type(1)
<class 'int'>

就連函式(function)也都有型別:

>>> def f(): pass
...
>>> type(f)
<class 'function'>

不過奇妙的事來了,當我們定義一個 class A 之後,試圖查看其型別,竟然不是預期的 <class 'A'> 而是 <class 'type'> :

>>> class A: pass
...
>>> type(A)
<class 'type'>

而且連 dict, set, list 等等的 type 都是 <class 'type'> :

>>> type(dict)
<class 'type'>
>>> type(list)
<class 'type'>

甚至連所有類別之母 object 的型別也是 <class 'type'> :

>>> type(object)
<class 'type'>

這是為什麼呢?

原來當我們定義 1 個類別時,舉類別 A 為例:

class A: pass

等同於執行以下程式碼:

A = type('A', (), {})

By default, classes are constructed using type()

是的, type() 函式不僅可以查看型別,還可以用來產生類別,而且它就是 class 預設的產生方式(文件),它的參數如下所示:

class type(name, bases, dict, **kwds)

這 3 個參數分別是:

  1. name 類別名稱
  2. bases 繼承的類別清單,必須是 tuple
  3. dict 1 個字典,裡面存類別的屬性及方法

再舉下列類別 A 為例,該類別繼承 dict, 而且有 1 個 name 屬性,其值為 A :

class A(dict):
	name = 'A'

如果用 type() 函式表示的話,如下所示:

A = type('A', (dict, ), dict(name='A'))

重點來了!

With three arguments, return a new type object.

根據官方文件所述, type() 函式回傳的結果是被稱為 type object 的 object, 而 type object 的型別就是 type (好像繞口令,但就是這樣) 。

>>> A = type('A', (dict, ), dict(name='A'))
>>> type(A)
<class 'type'>

這就是為什麼我們定義 class 之後,它的型別會是 <class 'type'> 的原因。

前述範例只展示如何用 type() 函式定義類別屬性,接下來展示如何用 type() 函式定義類別內的方法,以下列類別 A 為例,該類別繼承 dict, 而且有 1 個方法 console(self, x) :

class A(dict):
    def console(self, x):
        print(x)

如果用 type() 函式表示的話,如下所示,其實就是把方法變成函式,並在 type() 第 3 個參數內放入 1 個 key 為方法名, value 為函式的 dictionary:

def console(self, x):
    print(x)

A = type('A', (dict, ), dict(console=console))

實際執行結果如下,可以看到產生的類別與方法都正常運作:

>>> A().console('Hi')
Hi

看到這邊,大家應該會想到可以用 type() 函式動態產生類別吧!

Metaclass 介紹

前面的範例可以看到 type object 的型別是 <class 'type'> , 官方文件也確實寫 type 是個 class, 那理論上它也可以被繼承囉?

沒錯!

類別可以繼承 type 類別,而且繼承 type 的類別就被稱為 metaclass !

metaclass 有別於一般的類別,因為它管的是類別如何產生的這件事,也就是規定 Python 要怎麼產生類別,就跟 type() 做的事一樣,所以我們可以透過 metaclass 改寫類別產生的行為,例如 type.__new__ 方法就是規定類別要如何產生的的方法,透過改寫該方法,就能做到對類別加工的效果。

直接看範例會更好理解,下列定義 1 個名為 Meta 的 metaclass, 然後改寫 type.__new__ 方法,裏面把類別的屬性都列印出來:

class Meta(type):
    def __new__(cls, name, bases, attrs):
        print(attrs)
        return super().__new__(cls, name, bases, attrs)

class A(metaclass=Meta):
    X = 1

p.s. 此處應該會注意到 __new__ 的參數與 type() 函式相同,都是類別名稱、繼承清單、類別屬性

當我們執行上述程式碼時,連實例化 A 類別的過程都沒有,就會直接產生以下結果:

{'__module__': '__main__', '__qualname__': 'A', 'X': 1}

這是因為 metaclass 會作用在定義類別時,所以就算沒有實例化類別, metaclass 也會被執行,因為它做的就是讓 Python 按照它的規定產生類別。

這時我們再看一次類別 A 的 type, 就會發現它的 type 變成 <class '__main__.Meta'> , 就是因為我們換掉它的 metaclass 的緣故:

>>> type(A)
<class '__main__.Meta'>

讀到此處,大家應該可以想到可以透過 metaclass 介入類別的生成,例如下列範例在類別的生成過程,強行把 __str__ 方法換成客製化的 __str__ 方法,讓生成的類別會自動具備 1 個 __str__ 方法:

class Meta(type):
    def __new__(cls, name, bases, attrs):
        def __str__(self):
            return f"{name} (metaclass=Meta)"
        attrs['__str__'] = __str__
        return super().__new__(cls, name, bases, attrs)

class A(metaclass=Meta):
    X = 1

上述範例可以試著執行看看,可以發現類別 A 雖然沒實作 __str__, 但是我們透過 metaclass 把 __str__ 給注入了,所以列印 A() 時就會執行我們所注入的 return f"{name} (metaclass=Meta)" :

>>> print(A())
A (metaclass=Meta)

除了透過 metaclass 修改方法、屬性之外,其實也可以對繼承清單做加工,你可以任意拿掉或新增,下列範例就是為生成的類別強制繼承 dict, 使得生成的類別都可以像 dictionary 一樣使用:

class Meta(type):
    def __new__(cls, name, bases, attrs):
        new_bases = list(bases)
        new_bases.append(dict)
        return super().__new__(cls, name, tuple(new_bases), attrs)

class A(metaclass=Meta):
    X = 1

透過改寫繼承清單,上述範例的 A 類別就能像 dict 一樣使用:

>>> a = A()
>>> a['x'] = 1
>>> list(a.items())
[('x', 1)]

總結

雖然 metaclass 看似強大,但其實 99% 的時候都用不到它,絕大多數的需求都可透過繼承解決,除非你想動態生成類別或者在類別生成時做一些檢查、加工的工作,才有可能需要用到 metaclass 。

目前 metaclass 的運用主要還是在框架(framework)的開發上,例如 tensorflow, PyTorch, Django 都有用到,主要是 metaclass 對生成的類別做一些加工,譬如 Django 的 metaclass 會把繼承 Form 類別的屬性,透過解析 MRO 把所有欄位存到 declared_fields 這個屬性。

雖然多數人用不到,但它還是屬於 Python 程式的高級技巧,不免還是要了解一下囉!

Happy Coding!

References

Python - Data model

types — Dynamic type creation and names for built-in types

What are metaclasses in Python?

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!