Python: 陌生的 metaclass

原文出處: geekvi

元類

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

類也是對象

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

  • 把類賦值給一個變量
  • 把類作為函數參數進行傳遞
  • 把類作為函數的返回值
  • 在運行時動態地創建類

看一個簡單的例子:

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,我們經常使用它來判斷一個對象的類型,比如:

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(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性和方法的字典(名稱和值))
最簡單的情況

假設有下面的類:

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

PyChina將聯合JetBrain(出品PyCharm的公司)一起在北京舉辦一次Python沙龍活動。

時間:11月26日晚上19:00-21:00

地點:科技寺北新橋 北京市東城區東四北大街107號科林大廈B座107室(近北新橋地鐵站)

歡迎大家報名參加本次活動,特別需要志愿者來幫忙組織本次活動。

詳情請點擊此處

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容