Python: 陌生的 metaclass

元類

Python 中的元類(metaclass)是一個深度魔法,平時咱們可能比較少接觸到元類,本文將經過一些簡單的例子來理解這個魔法。python

類也是對象

在 Python 中,一切皆對象。字符串,列表,字典,函數是對象,類也是一個對象,所以你能夠:函數

  • 把類賦值給一個變量oop

  • 把類做爲函數參數進行傳遞網站

  • 把類做爲函數的返回值.net

  • 在運行時動態地建立類code

看一個簡單的例子:對象

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 來建立類。blog

這裏,出現了 type,沒錯,是你知道的 type,咱們常用它來判斷一個對象的類型,好比:繼承

class Foo(object):
    Foo = True

>>> type(10)
<type 'int'>
>>> type('hello')
<type 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<type 'type'>

事實上,type 除了能夠返回對象的類型,它還能夠被用來動態地建立類(對象)。下面,咱們看幾個例子,來消化一下這句話。ip

使用 type 來建立類(對象)的方式以下:

type(類名, 父類的元組(針對繼承的狀況,能夠爲空),包含屬性和方法的字典(名稱和值))

最簡單的狀況

假設有下面的類:

class Foo(object):
    pass

如今,咱們不使用 class 關鍵字來定義,而使用 type,以下:

Foo = type('Foo', (object, ), {})    # 使用 type 建立了一個類對象

上面兩種方式是等價的。咱們看到,type 接收三個參數:

  • 第 1 個參數是字符串 'Foo',表示類名

  • 第 2 個參數是元組 (object, ),表示全部的父類

  • 第 3 個參數是字典,這裏是一個空字典,表示沒有定義屬性和方法

在上面,咱們使用 type() 建立了一個名爲 Foo 的類,而後把它賦給了變量 Foo,咱們固然能夠把它賦給其餘變量,可是,此刻不必給本身找麻煩。

接着,咱們看看使用:

>>> print Foo
<class '__main__.Foo'>
>>> print Foo()
<__main__.Foo object at 0x10c34f250>

有屬性和方法的狀況

假設有下面的類:

class Foo(object):
    foo = True
    def greet(self):
        print 'hello world'
        print self.foo

type 來建立這個類,以下:

def greet(self):
    print 'hello world'
    print self.foo

Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})

上面兩種方式的效果是同樣的,看下使用:

>>> f = Foo()
>>> f.foo
True
>>> f.greet
<bound method Foo.greet of <__main__.Foo object at 0x10c34f890>>
>>> f.greet()
hello world
True

繼承的狀況

再來看看繼承的狀況,假設有以下的父類:

class Base(object):
    pass

咱們用 Base 派生一個 Foo 類,以下:

class Foo(Base):
   foo = True

改用 type 來建立,以下:

Foo = type('Foo', (Base, ), {'foo': True})

什麼是元類(metaclass)

元類(metaclass)是用來建立類(對象)的可調用對象。這裏的可調用對象能夠是函數或者類等。但通常狀況下,咱們使用類做爲元類。對於實例對象、類和元類,咱們能夠用下面的圖來描述:

類是實例對象的模板,元類是類的模板

+----------+             +----------+             +----------+
|          |             |          |             |          |
|          | instance of |          | instance of |          |
| instance +------------>+  class   +------------>+ metaclass|
|          |             |          |             |          |
|          |             |          |             |          |
+----------+             +----------+             +----------+

咱們在前面使用了 type 來建立類(對象),事實上,type 就是一個元類

那麼,元類到底有什麼用呢?要你何用...

元類的主要目的是爲了控制類的建立行爲。咱們仍是先來看看一些例子,以消化這句話。

元類的使用

先從一個簡單的例子開始,假設有下面的類:

class Foo(object):
    name = 'foo'
    def bar(self):
        print 'bar'

如今咱們想給這個類的方法和屬性名稱前面加上 my_ 前綴,即 name 變成 my_name,bar 變成 my_bar,另外,咱們還想加一個 echo 方法。固然,有不少種作法,這裏展現用元類的作法。

1.首先,定義一個元類,按照默認習慣,類名以 Metaclass 結尾,代碼以下:

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__ 的屬性,以下:

class Foo(object):
    __metaclass__ = PrefixMetaclass
    name = 'foo'
    def bar(self):
        print 'bar'

在 Python3 中,這樣作:

class Foo(metaclass=PrefixMetaclass):
    name = 'foo'
    def bar(self):
        print 'bar'

如今,讓咱們看看使用:

>>> 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,這就是元類的魔法。

再來看一個繼承的例子,下面是完整的代碼:

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。先讓咱們看看使用:

>>> 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 來建立它。

本文由 funhacks 發表於我的博客,採用 Creative Commons BY-NC-ND 4.0(自由轉載-保持署名-非商用-禁止演繹)協議發佈。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人。
本文標題爲: Python: 陌生的 metaclass
本文連接爲: https://funhacks.net/2016/11/...

參考資料

相關文章
相關標籤/搜索