1. 程式人生 > >Python中的類和元類(metaclass)以及黑魔法(__metaclass__)

Python中的類和元類(metaclass)以及黑魔法(__metaclass__)

一、Python中的類

首先在這裡討論的python類,都是基於新式類進行討論。
在python中,一切皆為物件。
在理解元類之前我們先來重新理解一下python中的類。

class Joker:
    pass

當Python在執行帶class語句的時候,會初始化一個類物件放在記憶體裡面。例如這裡會初始化一個Joker物件,這個類物件自身擁有建立例項物件的能力。

為了方便後續理解,我們可以嘗試一下在新式類中的關鍵字type。

class Joker:
    pass

print(type('123'))
print(type(123))
print(type(Joker()))

>>> <type 'str'>
>>> <type 'int'>
>>> <class '__main__.Joker'>

我們可以看到,type可以檢視一個物件的型別,也能夠檢視建立這個例項物件的類。

但是下面的方法你可能沒有見過,type同樣可以用來動態建立一個類。

type(類名,父類的元組(針對繼承的情況,可以為空),包含屬性的字典(名稱和值))

這個怎麼用呢,我們可以用這個方法建立一個類:

print(type('Joker', (), {}))

>>> <class '__main__.Joker'>

同樣我們可以例項化這個類物件:

print(type('Joker', (), {})())

>>> <__main__.joker object at xxxxxxxxx>

可以看到,這裡就是一個joker的例項物件。

同樣的這個方法還可以初始化建立類的父類,同時也可以初始化類屬性:

class Done:
    pass

dn = type('Joker', (Done, ), {'sleep': 'zzz'})

print(dn().sleep)
print(dn.__dict__)
print(dn.__bases__)
print(dn().__class__)
print(dn().__class__.__class__)

>>> zzz
>>> {'__module': '__main__', 'sleep': 'zzz', '__doc__': None}
>>> (<class '__main__.Done'>, )
>>> <class '__main__.Joker'>
>>> <type 'type'>

下面我們先來了解幾個魔法方法:
1. 方法__class__用於檢視物件是由誰建立的,類物件也是物件。類是元類的例項,而例項物件則是類的例項。
2. 方法__bases__用於得到一個物件的父類是誰,特別注意一下__base__返回單個父類,__bases__以元組的形式返回所有父類。

下面我來解釋一下上面程式碼的執行結果:
1. 使用type建立一個類賦值給dn,type接收的三個引數的意思分別為(類的名稱,類的父類(可以為空),類的屬性(字典形式))。
2. 初始化類的例項,然後嘗試去獲得類屬性的sleep屬性。
3. 獲取dn這個類的所有屬性(字典形式)。
4. 獲取dn這個類的父類集合。
5. 獲取建立dn()這個例項物件的類。
6. 獲取建立dn()這個例項物件的類的類(有點繞,即獲取例項化Joker這個類的類)。

二、什麼是元類以及簡單的運用

介紹完上面的內容後我們開始進入主要環節:
那到底什麼是元類呢?
通俗的說,元類就是建立類的類,是不是還是有點抽象?
不要慌,聽我慢慢道來:

Joker = MetaClass()
MyObject = Joker()

上面我們已經介紹了,建立一個類可以直接這樣搞:

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

那為什麼可以這樣來搞呢?
type實際上是一個元類,用它可以去建立類。
也可以說元類實際上就是一個建立類的工廠。

那麼類裡面的__metaclass__屬性,相信很多小朋友對這個很好奇,使用了它實際上就意味著會用__metaclass__指定的元類來建立類了。

class Joker(Done):
    pass

當我麼在建立上面的類的時候,Python做了如下的操作:
首先,Python會先去Joker裡面找是否有__metaclass__這個屬性,如果有,Python會在記憶體中通過__metaclass__建立一個名字為Joker的類物件,也就是Joker;如果沒有找到__metaclass__,它會繼續在自己的父類Done中尋找__metaclass__屬性,並且嘗試以__metaclass__指定的方法建立一個Joker類物件。
如果Python在任何一個父類中都沒有找到__metaclass__屬性,它會去模組中搜尋是否有__metaclass__屬性。如果還是找不到,Python會是哦用預設的type來建立Joker。

那麼問題來了,__metaclass__屬性中設定的是什麼呢?
答案是可以建立一個類的東西:type或者任何用到type或子類化type的東西都行。

三、自定義元類

自定義元類的目的,就是攔截類的建立,然後修改一些特性,然後返回該類。
是不是感覺和某種東西很像?沒錯,它和裝飾器很像,為特定的物件附加額外的功能,或者修改某些功能,最後被返回。

其實除了上面談到的指定一個__metaclass__,我們給它賦值的不一定是一個正式的類,函式也可以。
要建立一個使所有模組級別都是用這個元類建立的話,我們只需要在模組級別設定__metaclass__就可以了。
下面我們來寫個例子,將建立的類裡面的所有屬性名都改為大寫。

def upper_attr(class_name, class_parents, class_attr):
'''
返回一個物件,將它的屬性都改為大寫
:param class_name: 類的名稱
: param class_parents: 類的父類tuple
: param class_attr: 類的引數dict
: return: 類
'''
    # 使用一個generator來儲存類的引數
    attrs = ((name, value) for name, value in class_attr.items() if not name.startswith('__'))
    # 將屬性名全部改為大寫並轉化為dict
    uppercase_attrs = dict((name.upper(), value) for name, value in attrs)

    return type(class_name, class_parents, uppercase_attrs)

# 設定全域性變數
__metaclass__ = upper_attr

# 建立類
upa = upper_attr('Joker', (), {'bar': 0})

print(hasattr(upa, 'bar'))
print(hasattr(upa, 'BAR'))
print(upa.BAR)

>>> False
>>> True
>>> 0

我們可以看到在上面的程式碼中,實現了一個元類(metaclass),然後指定了模組使用這個元類來建立類,所以當我下面使用type進行建立的時候,可以發現小寫的bar引數被替換成了大寫的BAR引數,並且在最後可以呼叫這個被修改後的引數。

上面我們使用函式作為元類傳遞給類,下面我們使用一個正式類來作為元類傳遞給__metaclass__

class UpperAttrMetaClass(type):
    # 注意它的第一個引數為mcs,這個和類方法用cls、例項方法用self是同一個道理
    def __new__(mcs, class_name, class_parents, class_attr):
        attrs = ((name, value) for name, value in class_attr if not name.startswith('__'))

        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        return super(UpperAttrMetaClass, mcs).__new__(mcs, class_name, class_parents, uppercase_attr)


class Joker:
    __metaclass__ = UpperAttrMetaClass
    bar = 12
    money = 'unlimited'

print(Joker.BAR)
print(Joker.MONEY)

>>> 12
>>> unlimited

總結

這些都是我在蒐集了諸多資料之後自行整理的一些筆記,備忘的同時希望也能分享給大家,這個知識點大家沒有必要去死記硬背,在需要的時候可以回過頭來複習一下,平時的話很少有用到的地方,但是它能讓你瞭解在Python當中類建立的這麼一個過程,加深大家對Python的理解。