元類
Python 中的元類(metaclass)是一個深度魔法,平時我們可能比較少接觸到元類,本文將通過一些簡單的例子來理解這個魔法。
類也是物件
在 Python 中,一切皆物件。字串,列表,字典,函式是物件,類也是一個物件,因此你可以:
- 把類賦值給一個變數
- 把類作為函式引數進行傳遞
- 把類作為函式的返回值
- 在執行時動態地建立類
看一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Foo(object): foo = True class Bar(object): bar = True def echo(cls): print cls def select(name): if name == 'foo': return Foo # 返回值是一個類 if name == 'bar': return Bar >>> echo(Foo) # 把類作為引數傳遞給函式 echo <class '__main__.Foo'> >>> cls = select('foo') # 函式 select 的返回值是一個類,把它賦給變數 cls >>> cls __main__.Foo |
熟悉又陌生的 type
在日常使用中,我們經常使用 object
來派生一個類,事實上,在這種情況下,Python 直譯器會呼叫 type
來建立類。
這裡,出現了 type
,沒錯,是你知道的 type
,我們經常使用它來判斷一個物件的型別,比如:
1 2 3 4 5 6 7 8 9 10 11 |
class Foo(object): Foo = True >>> type(10) <type 'int'> >>> type('hello') <type 'str'> >>> type(Foo()) <class '__main__.Foo'> >>> type(Foo) <type 'type'> |
事實上,type
除了可以返回物件的型別,它還可以被用來動態地建立類(物件)。下面,我們看幾個例子,來消化一下這句話。
使用 type
來建立類(物件)的方式如下:
type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性和方法的字典(名稱和值))
最簡單的情況
假設有下面的類:
1 2 |
class Foo(object): pass |
現在,我們不使用 class
關鍵字來定義,而使用 type
,如下:
1 |
Foo = type('Foo', (object, ), {}) # 使用 type 建立了一個類物件 |
上面兩種方式是等價的。我們看到,type
接收三個引數:
- 第 1 個引數是字串 ‘Foo’,表示類名
- 第 2 個引數是元組 (object, ),表示所有的父類
- 第 3 個引數是字典,這裡是一個空字典,表示沒有定義屬性和方法
在上面,我們使用 type()
建立了一個名為 Foo 的類,然後把它賦給了變數 Foo,我們當然可以把它賦給其他變數,但是,此刻沒必要給自己找麻煩。
接著,我們看看使用:
1 2 3 4 |
>>> print Foo <class '__main__.Foo'> >>> print Foo() <__main__.Foo object at 0x10c34f250> |
有屬性和方法的情況
假設有下面的類:
1 2 3 4 5 |
class Foo(object): foo = True def greet(self): print 'hello world' print self.foo |
用 type
來建立這個類,如下:
1 2 3 4 5 |
def greet(self): print 'hello world' print self.foo Foo = type('Foo', (object, ), {'foo': True, 'greet': greet}) |
上面兩種方式的效果是一樣的,看下使用:
1 2 3 4 5 6 7 8 |
>>> f = Foo() >>> f.foo True >>> f.greet <bound method Foo.greet of <__main__.Foo object at 0x10c34f890>> >>> f.greet() hello world True |
繼承的情況
再來看看繼承的情況,假設有如下的父類:
1 2 |
class Base(object): pass |
我們用 Base 派生一個 Foo 類,如下:
1 2 |
class Foo(Base): foo = True |
改用 type
來建立,如下:
1 |
Foo = type('Foo', (Base, ), {'foo': True}) |
什麼是元類(metaclass)
元類(metaclass)是用來建立類(物件)的可呼叫物件。這裡的可呼叫物件可以是函式或者類等。但一般情況下,我們使用類作為元類。對於例項物件、類和元類,我們可以用下面的圖來描述:
1 2 3 4 5 6 7 8 9 |
類是例項物件的模板,元類是類的模板 +----------+ +----------+ +----------+ | | | | | | | | instance of | | instance of | | | instance +------------>+ class +------------>+ metaclass| | | | | | | | | | | | | +----------+ +----------+ +----------+ |
我們在前面使用了 type
來建立類(物件),事實上,type
就是一個元類。
那麼,元類到底有什麼用呢?要你何用…
元類的主要目的是為了控制類的建立行為。我們還是先來看看一些例子,以消化這句話。
元類的使用
先從一個簡單的例子開始,假設有下面的類:
1 2 3 4 |
class Foo(object): name = 'foo' def bar(self): print 'bar' |
現在我們想給這個類的方法和屬性名稱前面加上 my_
字首,即 name 變成 my_name,bar 變成 my_bar,另外,我們還想加一個 echo 方法。當然,有很多種做法,這裡展示用元類的做法。
1.首先,定義一個元類,按照預設習慣,類名以 Metaclass 結尾,程式碼如下:
1 2 3 4 5 6 7 8 9 |
class PrefixMetaclass(type): def __new__(cls, name, bases, attrs): # 給所有屬性和方法前面加上字首 my_ _attrs = (('my_' + name, value) for name, value in attrs.items()) _attrs = dict((name, value) for name, value in _attrs) # 轉化為字典 _attrs['echo'] = lambda self, phrase: phrase # 增加了一個 echo 方法 return type.__new__(cls, name, bases, _attrs) # 返回建立後的類 |
上面的程式碼有幾個需要注意的點:
- PrefixMetaClass 從
type
繼承,這是因為 PrefixMetaclass 是用來建立類的 __new__
是在__init__
之前被呼叫的特殊方法,它用來建立物件並返回建立後的物件,對它的引數解釋如下:- cls:當前準備建立的類
- name:類的名字
- bases:類的父類集合
- attrs:類的屬性和方法,是一個字典
2.接著,我們需要指示 Foo 使用 PrefixMetaclass 來定製類。
在 Python2 中,我們只需在 Foo 中加一個 __metaclass__
的屬性,如下:
1 2 3 4 5 |
class Foo(object): __metaclass__ = PrefixMetaclass name = 'foo' def bar(self): print 'bar' |
在 Python3 中,這樣做:
1 2 3 4 |
class Foo(metaclass=PrefixMetaclass): name = 'foo' def bar(self): print 'bar' |
現在,讓我們看看使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> f = Foo() >>> f.name # name 屬性已經被改變 --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-774-4511c8475833> in <module>() ----> 1 f.name AttributeError: 'Foo' object has no attribute 'name' >>> >>> f.my_name 'foo' >>> f.my_bar() bar >>> f.echo('hello') 'hello' |
可以看到,Foo 原來的屬性 name 已經變成了 my_name,而方法 bar 也變成了 my_bar,這就是元類的魔法。
再來看一個繼承的例子,下面是完整的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class PrefixMetaclass(type): def __new__(cls, name, bases, attrs): # 給所有屬性和方法前面加上字首 my_ _attrs = (('my_' + name, value) for name, value in attrs.items()) _attrs = dict((name, value) for name, value in _attrs) # 轉化為字典 _attrs['echo'] = lambda self, phrase: phrase # 增加了一個 echo 方法 return type.__new__(cls, name, bases, _attrs) class Foo(object): __metaclass__ = PrefixMetaclass # 注意跟 Python3 的寫法有所區別 name = 'foo' def bar(self): print 'bar' class Bar(Foo): prop = 'bar' |
其中,PrefixMetaclass 和 Foo 跟前面的定義是一樣的,只是新增了 Bar,它繼承自 Foo。先讓我們看看使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
>>> b = Bar() >>> b.prop # 發現沒這個屬性 --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-778-825e0b6563ea> in <module>() ----> 1 b.prop AttributeError: 'Bar' object has no attribute 'prop' >>> b.my_prop 'bar' >>> b.my_name 'foo' >>> b.my_bar() bar >>> b.echo('hello') 'hello' |
我們發現,Bar 沒有 prop 這個屬性,但是有 my_prop 這個屬性,這是為什麼呢?
原來,當我們定義 class Bar(Foo)
時,Python 會首先在當前類,即 Bar 中尋找 __metaclass__
,如果沒有找到,就會在父類 Foo 中尋找 __metaclass__
,如果找不到,就繼續在 Foo 的父類尋找,如此繼續下去,如果在任何父類都找不到 __metaclass__
,就會到模組層次中尋找,如果還是找不到,就會用 type 來建立這個類。
這裡,我們在 Foo 找到了 __metaclass__
,Python 會使用 PrefixMetaclass 來建立 Bar,也就是說,元類會隱式地繼承到子類,雖然沒有顯示地在子類使用 __metaclass__
,這也解釋了為什麼 Bar 的 prop 屬性被動態修改成了 my_prop。
寫到這裡,不知道你理解元類了沒?希望理解了,如果沒理解,就多看幾遍吧~
小結
- 在 Python 中,類也是一個物件。
- 類建立例項,元類建立類。
- 當你建立類時,直譯器會呼叫元類來生成它,定義一個繼承自 object 的普通類意味著呼叫 type 來建立它。