1. 程式人生 > >python的元類

python的元類

元類從概念上理解是不難的,僅從字面上去理解,當初學資料庫的時候,就知道有元資料這麼個東西,那麼元資料是什麼?元資料就是用來描述資料庫的資料。那麼一樣的道理,類是什麼?類是建立物件用的,那麼元類就是建立類用的。

一,先來看type方法

class Lan(object):
    def run(self):
        print('run...')

l = Lan()
print(type(l))
print(type(Lan))

Output:
<class '__main__.Lan'>
<class 'type'>

可以看到,l是通過Lan建立的物件,這個物件的型別是Lan。而Lan這個類本身的型別是type。

於是就有想法,Lan這個類可不可以通過type來建立?事實上是可以的,來看實現

def func(self):
    print('run...')

Lan = type('Lan', (object,), {'run':func})

l = Lan()
print(type(l))
print(type(Lan))

Output:
<class '__main__.Lan'>
<class 'type'>

這段通過type建立的程式碼實現了和前面程式碼一模一樣的功能,事實上,一般情況下,python直譯器在掃描到class的時候就是用type去建立它的。上面這段程式碼 不過是我們手動做了解析器的工作。

type的引數:

  1. class的名稱;
  2. 繼承的父類集合,注意Python支援多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
  3. class的方法名稱與函式繫結,這裡我們把函式fn繫結到方法名hello上。

到這一步,其實最需要關注的是,我們既然動態的建立了一個Lan類 那麼這個Lan類其實也是一個物件,type的物件。這也正是動態語言與靜態語言最大的不同,可以在執行時動態生成類,因為它也是個物件嘛,但是靜態語言如java就不行。可能你會說,java不是有反射嘛?但是請注意,反射也是根據.class檔案去獲取類物件,然後用這個類物件去例項化,去呼叫。類的定義在一早已經寫好了。而python這裡,是在執行前連類的定義都沒有。所以注意區分。

二,元類metaclass

元類是什麼開篇已經解釋過,現在來看怎麼用

class LanMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['fly'] = lambda self, value: print("%s can fly" % value)
        return type.__new__(cls, name, bases, attrs)

class Lan(object, metaclass=LanMetaclass):
    def run(self):
        print('run...')

l = Lan()
l.fly('bird')

Output:
bird can fly

是不是特別奇怪? 在Lan中我們只定義了run方法,可是Lan的物件l卻可以呼叫fly方法。這就是元類的用法,可以動態的修改類的定義。

分析一下是怎麼做到的:

前面我們說過,Lan是由直譯器呼叫type建立的,那麼這裡可以再深入一些,直譯器在掃描Lan的時候,發現有metaclass這個欄位,那麼就去呼叫相應的類(這裡是LanMetaclass)去建立類,也正是這個原因,讓它需要繼承type,最後實際還是交給了type去建立,我們不過是建立前,修改了類的定義。至於怎麼修改,看了__new__的四個引數就明白了:

  1. 當前準備建立的類的物件;
  2. 類的名字;
  3. 類繼承的父類集合;
  4. 類的屬性集合。

和type幾乎一模一樣,不過多了一個引數而已,所以用attrs去修改即可。

列印四個引數,加深理解:

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

class Lan(object, metaclass=LanMetaclass):
    def run(self):
        print('run...')

l = Lan()

Output:
<class '__main__.LanMetaclass'>
Lan
(<class 'object'>,)
{'__module__': '__main__', '__qualname__': 'Lan', 'run': <function Lan.run at 0x0000015EEA761950>}

看了上面的解釋,其實我對cls是什麼還是有疑惑的,然後就去查閱了官網文件,下面我們來捋一捋思路。

三,__new__和__init__兩個魔法方法

首先__init__是什麼? __init__(self, 引數)  目的就是初始化繫結引數,self就代指的是當前的物件。

那麼問題來了,self這個例項是哪裡來的?__new__(cls,...) 就是用來建立self這個例項的。寫一段程式碼來理解:

class Lan(object):
    def __new__(cls):
        print(cls)
        return object.__new__(cls)

l = Lan()

Output:
<class '__main__.Lan'>

很明顯,cls就是正在建立的Lan的物件,只不過這個具體的建立過程我們需要交給父類去完成,把這個類的資訊cls提供給父類方法去呼叫它的__new__就可以了。

其實這真是很簡單的概念,標題二里面之所以讓我看得比較費解,是因為把這個和元類混到一起去理解了就變得不清晰了。事實上,哪怕是元類,也不見得有那麼難,看過一篇文章有這樣的說法:元類,其實不難,難就難在當你要用到元類的時候,你要處理的問題很難。

所以現在可以更好的理解元類的工作流程:

當我們建立的類有metaclass欄位的時候,呼叫具體的Metaclass類,再它用__new__方法返回具體的例項之前,我們改變這個需要建立類的屬性,然後呼叫type去動態的建立了一個新的class。


思想是不是非常簡單,但是元類使用起來,就像上面說的,真的不簡單,因為你往往會處理很複雜的事情。

另外參考網文轉了一篇用metaclass寫一個orm框架的最基本原理,點我去看