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 個參數分別是:
name
類別名稱bases
繼承的類別清單,必須是 tupledict
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
types — Dynamic type creation and names for built-in types
What are metaclasses in Python?