04.05 深入理解 Python 元類

這是一篇在Stack overflow上很熱的帖子。提問者自稱已經掌握了有關 Python OOP 編程中的各種概念,但始終覺得元類(metaclass)難以理解。他知道這肯定和自省有關,但仍然覺得不太明白,希望大家可以給出一些實際的例子和代碼片段以幫助理解,以及在什麼情況下需要進行元編程。於是 e-satis 同學給出了神一般的回覆,該回復獲得了985點的贊同點數,更有人評論說這段回覆應該加入到 Python 的官方文檔中去。

類也是對象

在理解元類之前,你需要先掌握Python中的類。Python 中的類概念借鑑 Smalltalk,這顯得有些奇特。在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。當然在 Python 中這一點也是成立的。

>>> class ObjectCreator(object):
... pass
...
>>> my_object = ObjectCreator()
>>> print my_object
<__main__.objectcreator object="" at="">

但是,Python 中的類還遠不止如此,類同樣也是一種對象。只要你使用關鍵字class,Python 解釋器在執行的時候就會創建一個對象。下面的代碼段:

>>> class ObjectCreator(object):
... pass
...

將在內存中創建一個對象,名字就是 ObjectCreator,這個對象(類)自身擁有創建對象(類實例)的能力,而這就是為什麼它是一個類的原因。但是,它的本質仍然是一個對象,所以你就可以對它做如下的操作了:

  • 1) 你可以將它賦值給一個變量
  • 2) 你可以拷貝它
  • 3) 你可以為它增加屬性
  • 4) 你可以將它作為函數參數進行傳遞

如下示例:

>>> print ObjectCreator # 你可以打印一個類,因為它其實也是一個對象
<class>
>>> def echo(o):
... print o
...
>>> echo(ObjectCreator) # 你可以將類做為參數傳給函數
<class>
>>> print hasattr(ObjectCreator, 'new_attribute')
False
>>> ObjectCreator.new_attribute = 'foo' # 你可以為類增加屬性
>>> print hasattr(ObjectCreator, 'new_attribute')
True
>>> print ObjectCreator.new_attribute
foo
>>> ObjectCreatorMirror = ObjectCreator # 你可以將類賦值給一個變量
>>> print ObjectCreatorMirror()
<__main__.objectcreator object="" at="">
/<class>/<class>

動態地創建類

因為類也是對象,所以你可以在運行時動態的創建它們,就像其他任何對象一樣。首先,你可以在函數中創建類,使用class關鍵字即可。

>>> def choose_class(name):
... if name == 'foo':
... class Foo(object):
... pass
... return Foo # 返回的是類,不是類的實例
... else:
... class Bar(object):
... pass
... return Bar
...
>>> MyClass = choose_class('foo')
>>> print MyClass # 函數返回的是類,不是類的實例
<class>
>>> print MyClass() # 你可以通過這個類創建類實例,也就是對象
<__main__.foo object="" at="">
/<class>

但是這還不夠動態,因為你仍然需要自己編寫整個類的代碼,由於類也是對象,所以它們必須是通過什麼東西來生成的才對,當你使用class關鍵字的時候,Python 解釋器自動創建這個對象。和 Python 中的大多數事情一樣,Python 仍然提供給你手動處理的方法。還記得內建函數type嗎?這個古老但強大的函數能夠讓你知道一個對象的類型是什麼:

>>> print type(1)
<type>
>>> print type("1")
<type>
>>> print type(ObjectCreator)
<type>
>>> print type(ObjectCreator())
<class>
/<class>/<type>/<type>/<type>

在這裡,type有一種完全不同的能力,它也能動態的創建類,type 可以接受一個類的描述作為參數,然後返回一個類,type 可以像這樣工作:

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

比如下面的這一段代碼:

>>> class MyShinyClass(object):
… pass

可以使用 type 手動創建:

>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一個類對象
>>> print MyShinyClass
<class>
>>> print MyShinyClass() # 創建一個該類的實例
<__main__.myshinyclass object="" at="">
/<class>

你會發現我們使用"MyShinyClass"作為類名,並且也可以把它當做一個變量來作為類的引用。如果要定義屬性的話,比如下面的類:

>>> class Foo(object):
… bar = True

就可以通過 type 傳遞一個包含屬性的字典參數來創建上面的這個類:

>>> Foo = type('Foo', (), {'bar': True})

當然還是可以將 Foo 當成一個普通的類一樣使用:

>>> print Foo
<class>
>>> print Foo.bar
True
>>> f = Foo()
>>> print f
<__main__.foo object="" at="">
>>> print f.bar
True
/<class>

當然,如果還有繼承關係的話,如下面的類:

>>> class FooChild(Foo):
... pass

就可以通過下面的語句來創建這個類了:

>>> FooChild = type('FooChild', (Foo,), {})
>>> print FooChild
<class>
>>> print FooChild.bar # bar屬性是由Foo繼承而來
True
/<class>

如果你希望為你的類增加方法,只需要定一個恰當的函數並將其作為屬性賦值給該類對象即可:

>>> def echo_bar(self):
... print self.bar
...
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')

False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

你可以看到,在 Python 中,類也是對象,你可以動態的創建類,這就是當你使用關鍵字 class 時 Python 在幕後做的事情,而這就是通過元類(metaclass)來實現的。

元類(metaclass)是什麼?

元類就是用來創建類的,你創建類就是為了創建類的實例對象,不是嗎?但是我們上面已經知道了 Python 中的類也是對象,好吧,元類就是用來創建這些類(對象)的,元類就是類的類,你可以像下面這樣理解:

MyClass = MetaClass()
MyObject = MyClass()

上面我們已經提到可以使用 type 來創建類:

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

這是因為函數 type 實際上是一個元類,type 就是 Python 在背後用來創建所有類的元類。現在你想知道為什麼 type 會全部採用小寫形式而不是 Type 嗎?我猜這是為了和 str 保持一致性,str 是用來創建字符串對象的類,而 int 是用來創建整數對象的類。

type 就是創建類對象的類。你可以通過檢查__class__屬性來看到這一點,Python 中所有的東西,注意,我是指所有的東西 - 都是對象,這包括整數、字符串、函數以及類,它們全部都是對象,而且它們都是從一個類創建而來。

>>> age = 30
>>> age.__class__
<type>
>>> name = 'bob'
>>> name.__class__
<type>
>>> def foo(): pass
...
>>> foo.__class__
<type>
>>> class Bar(object): pass
...
>>> b = Bar()
>>> b.__class__
<class>
/<class>/<type>/<type>/<type>

現在我們再看看任意一個__class__的__class__屬性又是什麼呢?

>>> age.__class__.__class__
<type>
>>> foo.__class__.__class__
<type>
>>> b.__class__.__class__
<type>
/<type>/<type>/<type>

因此,元類就是創建類這種對象的東西,如果你喜歡的話,可以把元類稱為"類工廠",type 就是 Python 的內建元類,當然了,你也可以創建自己的元類。

metaclass 屬性

你可以在寫一個類的時候為其添加__metaclass__屬性:

class Foo(object):
__metaclass__ = something...

如果你這麼做了,Python 就會用元類來創建類 Foo。你首先寫下 class Foo(object),但是類對象 Foo 還沒有在內存中創建,Python 會在類的定義中尋找__metaclass__屬性,如果找到了,Python 就會用它來創建類 Foo,如果沒有找到,就會用內建的 type 來創建這個類。如下,當我們創建這樣的一個類時:

class Foo(Bar):
pass

Python 做了如下的一些操作:Foo 中有__metaclass__這個屬性嗎?如果有,Python 會在內存中通過__metaclass__創建一個名為 Foo 的類對象;如果沒有找到__metaclass__,它會繼續在 Bar(父類)中尋找該屬性,並嘗試做和前面同樣的操作;如果 Python 在任何父類中都找不到該屬性,它就會在模塊層次中去尋找,並嘗試做同樣的操作;如果還是找不到,那麼 Python 就會用內置的 type 來創建這個類對象了。

那麼我們可以在__metaclass__中放置些什麼樣的代碼呢?答案就是:可以創建一個類的東西。那麼什麼可以用來創建一個類呢?type,或者任何使用到 type 或者子類 type 的東西都可以。

自定義元類

元類的主要目的就是為了當創建類時能夠自動地改變類。通常,你會為 API 做這樣的事情,你希望可以創建符合當前上下文的類。比如你希望在你的模塊裡面的所有類的屬性都是大寫形式,有好幾種辦法可以辦到,但其中一種就是通過在模塊級別設定__metaclass__。採用這種方法,這個模塊中的所有類都會通過這個元類來創建,我們只需要告訴元類把所有的屬性都改成大寫形式就可以了。

__metaclass__實際上可以被任意調用,它並不是需要一個正式的類,所以,我們這裡就先以一個簡單的函數作為例子來說明,如下函數:

# 元類會自動將你通常傳給 type 的參數作為自己的參數傳入
def upper_attr(future_class_name, future_class_parents, future_class_attr):
"""返回一個類對象,將屬性都轉為大寫"""
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
# 將它們轉為大寫形式
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 通過 type 來做類對象的創建
return type(future_class_name, future_class_parents, upper_attr)
# 這會作用到這個模塊中的所有類
__metaclass__ = upper_attr
class Foo(object):
# 我們也可以在這裡定義__metaclass__,這樣就只會作用於這個類中

bar = 'bip'
print hasattr(Foo, 'bar')
# 輸出: False
print hasattr(Foo, 'BAR')
# 輸出:True
f = Foo()
print f.BAR
# 輸出:'bip'

現在讓我們再做一次,這一次用一個真正的 class 來當做元類:

# 記住,type 實際上是一個類,就想 str 和 int 一樣
# 所以,你可以從 type 繼承
class UpperAttrMetaClass(type):
# __new__ 是在 __init__ 之前被調用的特殊方法
# __new__ 是用來創建對象並返回的方法
# 而 __init__ 只是用來將傳入的參數初始化給對象
# 你很少用到 __new__,除非你希望能夠控制對象的創建
# 這裡,創建的對象是類,我們希望能夠自定義它,所以我們這裡重寫 __new__
# 如果你希望的話,也可以在 __init__ 中做一些事情
# 還有一些高級的用法會涉及到改寫 __call__ 特殊方法
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return type(future_class_name, future_class_parents, uppercase_attr)

但實際上上面這種方法不是很 OOP,我們直接調用了 type,而且我們沒有改寫父類的

new 方法,現在重新處理下:

class UpperAttrMetaclass(type):
def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
# 複用type.__new__方法,這就是基本的OOP編程,沒什麼魔法
return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)

你可能已經注意到了有個額外的參數 upperattr_metaclass,這並沒有什麼特別的,類方法的第一個參數總是表示當前的實例,就像在普通的類方法中的 self 參數一樣的,但是就像 self 一樣,所有的參數都有它們的傳統名稱,在實際的線上產品中一個元類應該像下面這樣更好:

class UpperAttrMetaclass(type):
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return type.__new__(cls, name, bases, uppercase_attr)

如果使用 super 方法的話,我們還可以使它變得更清晰一些:

class UpperAttrMetaclass(type):
def __new__(cls, name, bases, dct):
attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
uppercase_attr = dict((name.upper(), value) for name, value in attrs)
return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)

這就是關於元類的一些使用方法,使用到元類的代碼比較複雜,這並不是因為元類本身很複雜,而是因為通常會使用元類去做一些晦澀的事情,依賴於自身、控制繼承等等。使用元類來高一些魔法是很有用的,所以會搞一些複雜的東西出來,但就元類本身而已,它們其實很簡單的:

  • 1)攔截類的創建
  • 2)修改類
  • 3)返回修改之後的類

為什麼要用元類而不是函數?

由於__metaclass__可以接受任何可調用的對象,那為何還要使用類呢,因為很顯然使用類會更加複雜啊?這裡有好幾個原因:

  • 1)意圖會更加清晰。當你讀到 UpperAttrMetaclass(type) 時,你知道接下來要發生什麼。
  • 2)你可以使用OOP編程。元類可以從元類中繼承而來,改寫父類的方法。元類甚至還可以使用元類。
  • 3)你可以把代碼組織的更好。當你使用元類的時候肯定不會是像我上面舉的這種簡單場景,通常都是針對比較複雜的問題。將多個方法歸總到一個類中會很有幫助,也會使得代碼更容易閱讀。
  • 4)你可以使用 new, init
    以及 call這樣的特殊方法。它們能幫你處理不同的任務。就算通常你可以把所有的東西都在 new 裡處理掉,有些人還是覺得用 init 更舒服些。

為什麼要使用元類?

現在回到我們的大主題上來,究竟是為什麼你會去使用這樣一種容易出錯且晦澀的特性?好吧,一般來說,你根本就用不上它:

“元類就是深度的魔法,99%的用戶應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋為什麼要用元類。” —— Python 界的領袖 Tim Peters

元類的主要用途是創建 API。一個典型的例子是 Django ORM。它允許你像這樣定義:

class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()

但是如果你像這樣做的話:

guy = Person(name='bob', age='35')
print guy.age

這並不會返回一個 IntegerField 對象,而是會返回一個 int,甚至可以直接從數據庫中取出數據。這是因為 models.Model 定義了 metaclass, 並且使用了一些魔法能夠將你剛剛定義的簡單的 Person 類轉變成對數據庫的一個複雜 hook。Django 框架將這些看起來很複雜的東西通過暴露出一個簡單的使用元類的 API 將其化簡,通過這個 API 重新創建代碼,在背後完成真正的工作。

總結

首先,你要知道類其實是能夠創建出類實例的對象,另外,類本身也是實例,它們都是元類的實例

>>> class Foo(object): pass
...
>>> id(Foo)
140263822460752

Python 中的一切都是對象,它們要麼是類的實例,要麼是元類的實例,除了 type。type實際上是它自己的元類,在純 Python 環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。其次,元類是很複雜的。對於非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其他兩種技術來修改類:

  • 1)Monkey patching
  • 2)class decorators

當你需要動態修改類時,99%的時間裡你最好使用上面這兩種技術。當然了,其實在99%的時間裡你根本就不需要動態修改類。


分享到:


相關文章: