程式扎記: [ Python 文章收集 ] 深刻理解 Python 中的元類 (metaclass)

標籤

2015年2月10日 星期二

[ Python 文章收集 ] 深刻理解 Python 中的元類 (metaclass)

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

類也是對象
在理解 metaclass 之前,你需要先掌握 Python 中的類。Python 中類的概念借鑒於 Smalltalk,這顯得有些奇特。在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在 Python 中這一點仍然成立:
  1. class ObjectCreator(object):  
  2.     pass  
  3.   
  4. my_object = ObjectCreator()  
  5. print my_object  
但是,Python 中的類還遠不止如此。類同樣也是一種對象。是的,沒錯,就是對象。只要你使用關鍵字 class,Python 解釋器在執行的時候就會創建一個對象。下面的代碼段:
  1. class ObjectCreator(object):  
  2.     pass  
將在內存中創建一個對象,名字就是 ObjectCreator這個對象自身擁有創建對象類實例的能力,而這就是為什麼它是一個類的原因。但是,它的本質仍然是一個對象,於是乎你可以對它做如下的操作:
1) 你可以將它賦值給一個變量
2) 你可以拷貝它
3) 你可以為它增加屬性
4) 你可以將它作為函數參數進行傳遞

你可以參考下面範例更了解 metaclass 也是一個物件的事實:
  1. # 你可以打印一個類,因為它其實也是一個對象  
  2. print ObjectCreator # <class '__main__.ObjectCreator'>  
  3.   
  4. def echo(o):  
  5.     print o  
  6.   
  7. echo(ObjectCreator) # 你可以將類做為參數傳給函數  
  8.   
  9. print hasattr(ObjectCreator, 'new_attribute') # Fasle  
  10.   
  11. ObjectCreator.new_attribute = 'foo' # 你可以為類增加屬性  
  12. print hasattr(ObjectCreator, 'new_attribute') # True  
  13.   
  14. objectCreatorMirror = ObjectCreator # 你可以將類賦值給一個變量  
  15. print objectCreatorMirror() # <__main__ .objectcreator="" object...="">  
動態地創建類
因為類也是對象,你可以在運行時動態的創建它們,就像其他任何對像一樣。首先,你可以在函數中創建類,使用 class 關鍵字即可。
  1. def choose_class(name):  
  2.     if name == 'foo':  
  3.         class Foo(object):  
  4.             pass  
  5.         return Foo # 返回的是類,不是類的實例  
  6.     else:  
  7.         class Bar(object):  
  8.             pass  
  9.         return Bar  
  10.       
  11. MyClass = choose_class('foo')  
  12. print MyClass # 函數返回的是類,不是類的實例: <class '__main__.Foo'>  
  13. print MyClass() # 你可以通過這個類創建類實例,也就是對象: <__main__ .foo="" object...="">  
但這還不夠動態,因為你仍然需要自己編寫整個類的代碼。由於類也是對​​象,所以它們必須是通過什麼東西來生成的才對。當你使用 class 關鍵字時,Python 解釋器自動創建這個對象。但就和 Python 中的大多數事情一樣,Python 仍然提供給你手動處理的方法。還記得內建函數 type 嗎?這個古老但強大的函數能夠讓你知道一個對象的類型是什麼,就像這樣:
  1. print type(1)   # 'int'>  
  2. print type("1") # 'str'>  
  3. print type(ObjectCreator)   # 'type'>  
  • print type(ObjectCreator()) # <class '__main__.ObjectCreator'>  
  • 這裡,type 有一種完全不同的能力,它也能動態的創建類。type 可以接受一個類的描述作為參數,然後返回一個類。(我知道,根據傳入參數的不同,同一個函數擁有兩種完全不同的用法是一件很傻的事情,但這在Python中是為了保持向後兼容性). 用法如下:
    1. type(類名, 父類的元组(針對繼承的情况,可以為空),包含屬性的字典(名稱與值))  
    範例代碼如下:
    1. class MyShinyClass(object):  
    2.     pass  
    3.   
    4. MyShinyClass = type('MyShinyClass', (), {})  # 返回一个類對象: <class '__main__.MyShinyClass'>  
    5. print MyShinyClass  
    6. print MyShinyClass()  #  創建一個該類的實例: <__main__ .myshinyclass="" object...="">  
    你會發現我們使用 "My​​ShinyClass" 作為類名,並且也可以把它當做一個變量來作為類的引用。類和變量是不同的,這裡沒有任何理由把事情弄的複雜。type 接受一個字典來為類定義屬性,可以由下面代碼來了解此特性:
    1. class Foo(object):  
    2.     bar = True  
    3.       
    4. Foo2 = type('Foo', (), {'name':True})  
    5.   
    6. # <class '__main__.Foo'> has attr:bar? True, attr:name? False  
    7. print "%s has attr:bar? %s, attr:name? %s" % (Foo, hasattr(Foo, 'bar'), hasattr(Foo, 'name'))  
    8. # <class '__main__.Foo'> has attr:bar? False, attr:name? True  
    9. print "%s has attr:bar? %s, attr:name? %s" % (Foo2, hasattr(Foo2, 'bar'), hasattr(Foo2, 'name'))  
    並且可以將使用 type 創建出來的 Foo2 當成一個普通的類一樣使用:
    1. f2 = Foo2()  
    2. print "f2.name=%s; Foo2.name=%s" % (f2.name, Foo2.name)  
    3. f2.name = 'Peter'  
    4. print "f2.name=%s; Foo2.name=%s" % (f2.name, Foo2.name)  
    執行結果:
    Foo2.name=John
    f2.name=John; Foo2.name=John
    f2.name=Peter; Foo2.name=John

    當然,你可以向這個類繼承,所以,如下的代碼:
    1. class FooChild(Foo):  
    2.     pass  
    可以寫成:
    1. FooChild = type('FooChild', (Foo,),{})  
    2. print FooChild              # <class '__main__.FooChild'>  
    3. print FooChild.__base__     # <class '__main__.Foo'>  
    4. print FooChild.bar          # bar 屬性是從 Foo 繼承而來  
    最終你會希望為你的類增加方法。只需要定義一個有著恰當簽名的函數並將其作為屬性賦值就可以了:
    1. def echo_bar(self):  
    2.     print self.bar  
    3. FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})  
    4. print "Foo has method:echo_bar? %s" % (hasattr(Foo, 'echo_bar'))  
    5. print "FooChild has method:echo_bar? %s" % (hasattr(FooChild, 'echo_bar'))  
    6. my_foo = FooChild()  
    7. my_foo.echo_bar()  
    執行結果:
    Foo has method:echo_bar? False
    FooChild has method:echo_bar? True
    True

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

    到底什麼是元類
    metaclass (元類就是用來創建類的“東西”。你創建類就是為了創建類的實例對象,不是嗎?但是我們已經學習到了Python中的類也是對象。好吧,元類就是用來創建這些類(對象)的,元類就是類的類,你可以這樣理解為:
    1. MyClass = MetaClass()  # MetaClass 建立 Class  
    2. MyObject = MyClass() # Class 建立 Object  
    你已經看到了 type 可以讓你像這樣做
    1. MyClass = type('MyClass', (), {})  
    這是因為函數 type 實際上是一個元類type 就是 Python在背後用來創建所有類的元類。現在你想知道那為什麼 type 會全部採用小寫形式而不是 Type 呢?好吧,我猜這是為了和 str保持一致性,str 是用來創建字符串對象的類,而 int 是用來創建整數對象的類。type 就是創建類對象的類。你可以通過檢查 __class__ 屬性來看到這一點。Python 中所有的東西,注意,我是指所有的東西——都是對象。這包括整數、字符串、函數以及類。它們全部都是對象,而且它們都是從一個類創建而來。


    現在,對於任何一個 __class__ 的 __class__ 屬性又是什麼呢?


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

    __metaclass__ 屬性
    你可以在寫一個類的時候為其添加 __metaclass__ 屬性。
    1. class Foo(object):  
    2.     __metaclass__ = something…  
    3. […]  
    如果你這麼做了,Python 就會用元類來創建類 Foo。小心點,這裡面有些技巧。你首先寫下 class Foo(object),但是類對象 Foo 還沒有在內存中創建。Python 會在類的定義中尋找__metaclass__ 屬性,如果找到了,Python 就會用它來創建類 Foo,如果沒有找到,就會用內建的 type 來創建這個類。當你寫如下代碼時:
    1. class Foo(Bar):  
    2.     pass  
    Python 會做如下的操作:
    Foo 中有 __metaclass__ 這個屬性嗎?如果是,Python會在內存中通過 __metaclass__ 創建一個名字為 Foo 的類對象(我說的是類對象,請緊跟我的思路)。如果 Python沒有找到 __metaclass__,它會繼續在 Bar父類)中尋找 __metaclass__ 屬性,並嘗試做和前面同樣的操作。如果Python在任何父類中都找不到 __metaclass__,它就會在模塊層次中去尋找__metaclass__,並嘗試做同樣的操作。如果還是找不到 __metaclass__ , Python就會用內置的type 來創建這個類對象。

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

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

    幸運的是,__metaclass__ 實際上可以被任意調用,它並不需要是一個正式的類(我知道,某些名字裡帶有'class'的東西並不需要是一個class,畫畫圖理解下,這很有幫助)。所以,我們這裡就先以一個簡單的函數作為例子開始。
    1. # -*- coding: utf-8 -*-  
    2. # 元類會自動將你通常傳给 'type' 的參數作為自己的參數傳入  
    3. def upper_attr(future_class_name, future_class_parents, future_class_attr):  
    4.     '''返回一个类对象,将属性都转为大写形式'''  
    5.     #  選擇所有不以'__'開頭的屬性  
    6.     attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))  
    7.     # 将它们转为大写形式  
    8.     uppercase_attr = dict((name.upper(), value) for name, value in attrs)  
    9.   
    10.     # 通過 'type' 來做類對象的創建  
    11.     return type(future_class_name, future_class_parents, uppercase_attr)  
    12.   
    13. class Foo(object):  
    14.     # 在這裡定義 __metaclass__,這樣就只會作用於這個類中  
    15.     __metaclass__ = upper_attr  
    16.           
    17.     bar = 'bip'  
    18.       
    19. print hasattr(Foo, 'bar')  
    20. # 输出: False  
    21. print hasattr(Foo, 'BAR')  
    22. # 输出:True  
    23.   
    24. f = Foo()  
    25. print f.BAR  
    26. # 输出:'bip'  
    現在讓我們再做一次,這一次用一個真正的 class 來當做元類:
    1. # 請記住,'type' 實際上是一個類,就像 'str' 和 'int' 一樣  
    2. # 所以,你可以從 type 繼承  
    3. class UpperAttrMetaClass(type):  
    4.     # __new__ 是在__init__之前被調用的特殊方法  
    5.     # __new__ 是用来創建對象並返回之的方法  
    6.     # 而 __init__ 只是用来將傳入的参數初始化给對象  
    7.     # 你很少用到 __new__,除非你希望能够控制對象的創建  
    8.     # 這里,創建的對象是類,我们希望能够自定義它,所以我們這裡改寫 __new__  
    9.     # 如果你希望的话,你也可以在 __init__ 中做些事情  
    10.     # 還有一些高级的用法會涉及到改寫 __call__ 特殊方法,但是我们這里不用  
    11.     def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):  
    12.         attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))  
    13.         uppercase_attr = dict((name.upper(), value) for name, value in attrs)  
    14.         return type(future_class_name, future_class_parents, uppercase_attr)  
    但是,這種方式其實不是 OOP。我們直接調用了 type,而且我們沒有改寫父類的 __new__ 方法。現在讓我們這樣去處理:
    1. class UpperAttrMetaClass(type):  
    2.     # __new__ 是在__init__之前被調用的特殊方法  
    3.     # __new__ 是用来創建對象並返回之的方法  
    4.     # 而 __init__ 只是用来將傳入的参數初始化给對象  
    5.     # 你很少用到 __new__,除非你希望能够控制對象的創建  
    6.     # 這里,創建的對象是類,我们希望能够自定義它,所以我們這裡改寫 __new__  
    7.     # 如果你希望的话,你也可以在 __init__ 中做些事情  
    8.     # 還有一些高级的用法會涉及到改寫 __call__ 特殊方法,但是我们這里不用  
    9.     def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):  
    10.         attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))  
    11.         uppercase_attr = dict((name.upper(), value) for name, value in attrs)  
    12.   
    13.         # 重用 type.__new__ 方法  
    14.         # 這就是基本的 OOP 编程  
    15.         return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)  
    你可能已經註意到了有個額外的參數upperattr_metaclass,這並沒有什麼特別的。類方法的第一個參數總是表示當前的實例,就像在普通的類方法中的 self 參數一樣。當然了,為了清晰起見,這裡的名字我起的比較長。但是就像 self 一樣,所有的參數都有它們的傳統名稱。因此,在真實的產品代碼中一個元類應該是像這樣的:
    1. class UpperAttrMetaclass(type):  
    2.     def __new__(cls, name, bases, dct):  
    3.         attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')  
    4.         uppercase_attr  = dict((name.upper(), value) for name, value in attrs)  
    5.         return type.__new__(cls, name, bases, uppercase_attr)  
    如果使用 super 方法的話,我們還可以使它變得更清晰一些,這會緩解繼承(是的,你可以擁有元類,從元類繼承,從 type 繼承):
    1. class UpperAttrMetaclass(type):  
    2.     def __new__(cls, name, bases, dct):  
    3.         attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))  
    4.         uppercase_attr = dict((name.upper(), value) for name, value in attrs)  
    5.         return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)  
    就是這樣,除此之外,關於元類真的沒有別的可說的了。使用到元類的代碼比較複雜,這背後的原因倒並不是因為元類本身,而是因為你通常會使用元類去做一些晦澀的事情,依賴於自省,控制繼承等等。確實,用元類來搞些“黑暗魔法”是特別有用的,因而會搞出些複雜的東西來。但就元類本身而言,它們其實是很簡單的:
    1) 攔截類的創建
    2) 修改類
    3) 返回修改之後的類

    為什麼要用 metaclass 類而不是函數?
    由於 __metaclass__ 可以接受任何可調用的對象,那為何還要使用類呢,因為很顯然使用類會更加複雜啊?這裡有好幾個原因:
    1) 意圖會更加清晰。當你讀到 UpperAttrMetaclass(type) 時,你知道接下來要發生什麼。
    2) 你可以使用OOP編程。元類可以從元類中繼承而來,改寫父類的方法。元類甚至還可以使用元類。
    3) 你可以把代碼組織的更好。當你使用元類的時候肯定不會是像我上面舉的這種簡單場景,通常都是針對比較複雜的問題。將多個方法歸總到一個類中會很有幫助,也會使得代碼更容易閱讀。
    4) 你可以使用 __new____init__ 以及 __call__ 這樣的特殊方法。它們能幫你處理不同的任務。就算通常你可以把所有的東西都在 __new__ 里處理掉,有些人還是覺得用__init__ 更舒服​​些。
    5) 哇哦,這東西的名字是 metaclass,肯定非善類,我要小心!

    究竟為什麼要使用元類?
    現在回到我們的大主題上來,究竟是為什麼你會去使用這樣一種容易出錯且晦澀的特性?好吧,一般來說,你根本就用不上它:
    元類就是深度的魔法,99%的用戶應該根本不必為此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋為什麼要用元類。” —— Python界的領袖 Tim Peters

    元類的主要用途是創建 API。一個典型的例子是 Django ORM。它允許你像這樣定義:
    1. class Person(models.Model):  
    2.     name = models.CharField(max_length=30)  
    3.     age = models.IntegerField()  
    但是如果你像這樣做的話:
    1. guy  = Person(name='bob', age='35')  
    2. print guy.age  
    這並不會返回一個 IntegerField 對象,而是會返回一個 int,甚至可以直接從數據庫中取出數據。這是有可能的,因為mode​​ls.Model 定義了__metaclass__, 並且使用了一些魔法能夠將你剛剛定義的簡單的 Person 類轉變成對數據庫的一個複雜 hook。Django 框架將這些看起來很複雜的東西通過暴露出一個簡單的使用元類的 API 將其化簡,通過這個 API 重新創建代碼,在背後完成真正的工作

    結語
    首先,你知道了 類 其實是能夠創建出 類實例 的對象。好吧,事實上類本身也是實例,當然,它們是 metaclass 的實例。Python 中的一切都是對象,它們要么是類的實例,要么是元類的實例,除了 typetype 實際上是它自己的元類,在純 Python 環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。其次,元類是很複雜的。對於非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其他兩種技術來修改類:
    1)Monkey patching
    2) class decorators

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

    Supplement
    Stackoverflow - What is a metaclass in Python?
    The Python Tutorial » Classes
    The Python Language Reference » Data model
    Objects are Python’s abstraction for data. All data in a Python program is represented by objects or by relations between objects...


    沒有留言:

    張貼留言

    網誌存檔

    關於我自己

    我的相片
    Where there is a will, there is a way!