什麼是元類

 

元類是什麼

Python程式設計師經常說一句話:「一切皆對象」,意思是在Python中,你能見到的所有東西,包括int, float, function等等都是對象。但是在日常的開發中,當說到對象的時候,我們可能不會馬上就想到類。實際上類也是對象,既然類也是對象,那麼就存在一種途徑來創建一個類,這就是元類出場的地方,元類就是創建類的類。
 

元類做了什麼

元類會攔截類的創建過程,對類進行修改,然後返回修改後的類。
僅僅看上面這一句話,元類似乎很簡單,但是由於元類可以改變一個類的創建過程,可以在其中做一些奇技淫巧的事情,就會讓整個事情變得異常晦澀難懂。
 
type是python中一切類的元類,對一個對象一直調用type(),就會發現最終都會指向type, type比較特殊,type自己是自己的元類。
可以使用type這樣創建一個類
class Base:
    def __repr__(self):
    return self.__class__.__name__
 
 
def hello(self):
    print("hello")


Test = type("Test", (Base,), {"hello": hello})
其中,type接受的第一個參數是類的名字,第二個參數是一個元組,用來指定需要繼承的類,第三個參數是一個字典,在這個字典裡面可以把一個類需要的屬性、方法用一個字典放進去,這樣所有這個元類生成的類都會具有這些屬性。
上面的程式碼等效於下面的程式碼:
class Test(Base):
    def hello(self):
    print("hello")
此外,也可以通過type創建自己的元類,然後用這個元類來創建類:
class Meta(type):
    def __new__(mcs, name, bases, attrs):
        for k in list(attrs.keys()):
            if k.lower() != k:
                # 強制轉換屬性名稱到小寫
                attrs[k.lower()] = attrs.pop(k)
        return super(Meta, mcs).__new__(mcs, name, bases, attrs)

class Case(metaclass=Meta): def Hello(self): print("hello")
在上面的元類中我們對類的屬性進行了檢查,如果類的屬性不是小寫的(不符合PEP8 風格),那麼我們強制把類的屬性轉換成小寫。通過這種方式,我們強制子類符合一定的編碼風格(子類的屬性都必須是小寫),這只是元類應用中的hello word,使用元類還可以做更多其它的事情。
 
我們來實現一個稍微複雜一點的需求。
我們知道Python中的namedtuple可以很方便地用來表述一條數據,假設我們要用元類實現一個namedtuple,為了簡單起見,我們只要求這個實現可以接受位置參數,一個可能的實現是這樣的:
import itertools
import operator
 
 
def __new__(cls, *args, **kwargs):
    return tuple().__new__(cls, args)
 
 
def namedtuple(name, fields):
    fields = fields.split(",") if isinstance(fields, str) else fields
    attrs = {fld: property(itemgetter(i)) for i, fld in enumerate(fields)}
    attrs["__new__"] = __new__
    return type(name, (tuple,), attrs)
 
Student = namedtuple("Student", ["id", "name", "score"])
這樣,我們就擁有了一個簡單的具名元組,我們可以像使用Python的namedtuple一樣進行使用
stu = Student(1, "zhangsan", 100) # 這個實現並沒有支援關鍵字參數
print(stu.id)

元類中需要注意的幾個方法

元類中,我們通常通過定義__new__或者__init__或者__call__來控制類的創建過程,__new__在類創建之前調用,可以在類創建之前進行一些修改操作,__init__在類被創建之後,對被創建的類進行一些修改,__call__在實例化類的時候調用,通常情況下,我們只需要定義其中一個就足夠了,具體使用哪個,可以根據業務場景進行選擇。
此外,如果需要接受額外的參數,我們還需要定義__prepare__方法,這個方法會為類準備命名空間,但是通常都不需要定義這個方法,使用默認的就可以了。
比如我們要用元類實現一個單例模式,下面是一個簡單的例子:
class Singleton(type):
    __instance = None
 
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
        else:
             # 如果實例已經存在,直接返回
             return cls.__instance
 
 
class Test(metaclass=Singleton):
    def __init__(self):
        print('init in Test class')
 
 
if __name__ == "__main__":
    Test()
Test()
在上面的例子中,我們在元類中實現了__call__方法,在對類實例化的時候進行檢查,如果類已經有實例了,我們就返回已有的實例,保證類只會實例化一次。當然這個元類還有很多缺陷,比如多個類都用這個元類的時候,實際上實例會混淆,這是另一個問題,先不在這裡展開。
 

元類怎麼用

答案就是:通常情況下你不需要使用元類
關於元類的使用,Python裡面有一個廣泛傳播的解釋,原版如下:
Metaclasses are deeper magic that 99% of users should never worry about it. If you wonder whether you need them, you don’t (the people who actually need them to know with certainty that they need them and don’t need an explanation about why). ——Tim Peters
在我們開發工作中,我們實際很少會遇到需要需要動態創建一個類的地方,萬一我們遇到了需要動態改變一個類的時候,我們還可以通過類裝飾器和猴子修補程式實現,這兩種方法相比於元類都更加容易理解。
 

小結

上面的例子只是元類使用場景裡面的一些極簡示例,主要在於對元類有一個初始的認知,真正用到元類的場景及其實現都會比上面的例子複雜很多,目前,我們只需要知道下面兩個點就可以了。
  • 類創建實例,元類創建類,元類的實例是類
  • 一般情況下,不需要使用元類
Tags: