Python中的Classes和Metaclasses詳解

字號(hào):


    類和對(duì)象
    類和函數(shù)一樣都是Python中的對(duì)象。當(dāng)一個(gè)類定義完成之后,Python將創(chuàng)建一個(gè)“類對(duì)象”并將其賦值給一個(gè)同名變量。類是type類型的對(duì)象(是不是有點(diǎn)拗口?)。
    類對(duì)象是可調(diào)用的(callable,實(shí)現(xiàn)了 __call__方法),并且調(diào)用它能夠創(chuàng)建類的對(duì)象。你可以將類當(dāng)做其他對(duì)象那么處理。例如,你能夠給它們的屬性賦值,你能夠?qū)⑺鼈冑x值給一個(gè)變量,你可以在任何可調(diào)用對(duì)象能夠用的地方使用它們,比如在一個(gè)map中。事實(shí)上當(dāng)你在使用map(str, [1,2,3])的時(shí)候,是將一個(gè)整數(shù)類型的list轉(zhuǎn)換為字符串類型的list,因?yàn)閟tr是一個(gè)類??梢钥纯聪旅娴拇a:
    >>> class C(object):
    ... def __init__(self, s):
    ... print s
    ...
    >>> myclass = C
    >>> type(C)
    <type 'type'>
    >>> type(myclass)
    <type 'type'>
    >>> myclass(2)
    <__main__.C object at 0x10e2bea50>
    >>> map(myclass, [1,2,3])
    [<__main__.C object at 0x10e2be9d0>, <__main__.C object at 0x10e2bead0>, <__main__.C object at 0x10e2beb10>]
    >>> map(C, [1,2,3])
    [<__main__.C object at 0x10e2be950>, <__main__.C object at 0x10e2beb50>, <__main__.C object at 0x10e2beb90>]
    >>> C.test_attribute = True
    >>> myclass.test_attribute
    True
    正因如此,Python中的“class”關(guān)鍵字不像其他語言(例如C++)那樣必須出現(xiàn)在代碼main scope中。在Python中,它能夠在一個(gè)函數(shù)中嵌套出現(xiàn),舉個(gè)例子,我們能夠這樣在函數(shù)運(yùn)行的過程中動(dòng)態(tài)的創(chuàng)建類。看代碼:
    >>> def make_class(class_name):
    ... class C(object):
    ... def print_class_name(self):
    ... print class_name
    ... C.__name__ = class_name
    ... return C
    ...
    >>> C1, C2 = map(make_class, ["C1", "C2"])
    >>> c1, c2 = C1(), C2()
    >>> c1.print_class_name()
    C1
    >>> c2.print_class_name()
    C2
    >>> type(c1)
    <class '__main__.C1'>
    >>> type(c2)
    <class '__main__.C2'>
    >>> c1.print_class_name.__closure__
    (<cell at 0x10ab6dbe8: str object at 0x10ab71530>,)
    請(qǐng)注意,在這里通過make_class創(chuàng)建的兩個(gè)類是不同的對(duì)象,因此通過它們創(chuàng)建的對(duì)象就不屬于同一個(gè)類型。正如我們?cè)谘b飾器中做的那樣,我們?cè)陬惐粍?chuàng)建之后手動(dòng)設(shè)置了類名。同樣也請(qǐng)注意所創(chuàng)建類的print_class_name方法在一個(gè)closure cell中捕捉到了類的closure和class_name。如果你對(duì)closure的概念還不是很清楚,那么最好去看看前篇,復(fù)習(xí)一下closures和decorators相關(guān)的內(nèi)容。
    Metaclasses
    如果類是能夠制造對(duì)象的對(duì)象,那制造類的對(duì)象又該叫做什么呢(相信我,這并不是一個(gè)先有雞還是先有蛋的問題)?答案是元類(Metaclasses)。大部分常見的基礎(chǔ)元類都是type。當(dāng)輸入一個(gè)參數(shù)時(shí),type將簡(jiǎn)單的返回輸入對(duì)象的類型,這就不涉及元類。然而當(dāng)輸入三個(gè)參數(shù)時(shí),type將扮演元類的角色,基于輸入?yún)?shù)創(chuàng)建一個(gè)類并返回。輸入?yún)?shù)相當(dāng)簡(jiǎn)單:類名,父類及其參數(shù)的字典。后面兩者可以為空,來看一個(gè)例子:
    >>> MyClass = type("MyClass", (object,), {"my_attribute": 0})
    >>> type(MyClass)
    <type 'type'>
    >>> o = MyClass()
    >>> o.my_attribute
    0
    特別注意第二個(gè)參數(shù)是一個(gè)tuple(語法看起來很奇怪,以逗號(hào)結(jié)尾)。如果你需要在類中安排一個(gè)方法,那么創(chuàng)建一個(gè)函數(shù)并且將其以屬性的方式傳遞作為第三個(gè)參數(shù),像這樣:
    >>> def myclass_init(self, my_attr):
    ... self.my_attribute = my_attr
    ...
    >>> MyClass = type("MyClass", (object,), {"my_attribute": 0, "__init__": myclass_init})
    >>> o = MyClass("Test")
    >>> o.my_attribute
    'Test'
    >>> o.__init__
    <bound method MyClass.myclass_init of <__main__.MyClass object at 0x10ab72150>>
    我們可以通過一個(gè)可調(diào)用對(duì)象(函數(shù)或是類)來自定義元類,這個(gè)對(duì)象需要三個(gè)輸入?yún)?shù)并返回一個(gè)對(duì)象。這樣一個(gè)元類在一個(gè)類上實(shí)現(xiàn)只要定義了它的__metaclass__屬性。第一個(gè)例子,讓我們做一些有趣的事情看看我們能夠用元類做些什么:
    >>> def mymetaclass(name, parents, attributes):
    ... return "Hello"
    ...
    >>> class C(object):
    ... __metaclass__ = mymetaclass
    ...
    >>> print C
    Hello
    >>> type(C)
    <type 'str'>
    請(qǐng)注意以上的代碼,C只是簡(jiǎn)單地將一個(gè)變量引用指向了字符串“Hello”。當(dāng)然了,沒人會(huì)在實(shí)際中寫這樣的代碼,這只是為了演示元類的用法而舉的一個(gè)簡(jiǎn)單例子。接下來我們來做一些更有用的操作。在本系列的第二部分我們?cè)吹饺绾问褂醚b飾器類來記錄目標(biāo)類每個(gè)方法的輸出,現(xiàn)在我們來做同樣的事情,不過這一次我們使用元類。我們借用之前的裝飾器定義:
    def log_everything_metaclass(class_name, parents, attributes):
    print "Creating class", class_name
    myattributes = {}
    for name, attr in attributes.items():
    myattributes[name] = attr
    if hasattr(attr, '__call__'):
    myattributes[name] = logged("%b %d %Y - %H:%M:%S",
    class_name + ".")(attr)
    return type(class_name, parents, myattributes)
    class C(object):
    __metaclass__ = log_everything_metaclass
    def __init__(self, x):
    self.x = x
    def print_x(self):
    print self.x
    # Usage:
    print "Starting object creation"
    c = C("Test")
    c.print_x()
    # Output:
    Creating class C
    Starting object creation
    - Running 'C.__init__' on Aug 05 2013 - 13:50:58
    - Finished 'C.__init__', execution time = 0.000s
    - Running 'C.print_x' on Aug 05 2013 - 13:50:58
    Test
    - Finished 'C.print_x', execution time = 0.000s
    如你所見,類裝飾器與元類有著很多共同點(diǎn)。事實(shí)上,任何能夠用類裝飾器完成的功能都能夠用元類來實(shí)現(xiàn)。類裝飾器有著很簡(jiǎn)單的語法結(jié)構(gòu)易于閱讀,所以提倡使用。但就元類而言,它能夠做的更多,因?yàn)樗陬惐粍?chuàng)建之前就運(yùn)行了,而類裝飾器則是在類創(chuàng)建之后才運(yùn)行的。記住這點(diǎn),讓我們來同時(shí)運(yùn)行一下兩者,請(qǐng)注意運(yùn)行的先后順序:
    def my_metaclass(class_name, parents, attributes):
    print "In metaclass, creating the class."
    return type(class_name, parents, attributes)
    def my_class_decorator(class_):
    print "In decorator, chance to modify the class."
    return class_
    @my_class_decorator
    class C(object):
    __metaclass__ = my_metaclass
    def __init__(self):
    print "Creating object."
    c = C()
    # Output:
    In metaclass, creating the class.
    In decorator, chance to modify the class.
    Creating object.
    元類的一個(gè)實(shí)際用例
    讓我們來考慮一個(gè)更有用的實(shí)例。假設(shè)我們正在構(gòu)思一個(gè)類集合來處理MP3音樂文件中使用到的ID3v2標(biāo)簽Wikipedia。簡(jiǎn)而言之,標(biāo)簽由幀(frames)組成,而每幀通過一個(gè)四字符的識(shí)別碼(identifier)進(jìn)行標(biāo)記。舉個(gè)例子,TOPE標(biāo)識(shí)了原作者幀,TOAL標(biāo)識(shí)了原專輯名稱等。如果我們希望為每個(gè)幀類型寫一個(gè)單獨(dú)的類,并且允許ID3v2標(biāo)簽庫用戶自定義他們自己的幀類。那么我們可以使用元類來實(shí)現(xiàn)一個(gè)類工廠模式,具體實(shí)現(xiàn)方式可以這樣:
    frametype_class_dict = {}
    class ID3v2FrameClassFactory(object):
    def __new__(cls, class_name, parents, attributes):
    print "Creating class", class_name
    # Here we could add some helper methods or attributes to c
    c = type(class_name, parents, attributes)
    if attributes['frame_identifier']:
    frametype_class_dict[attributes['frame_identifier']] = c
    return c
    @staticmethod
    def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)
    class ID3v2Frame(object):
    frame_identifier = None
    __metaclass__ = ID3v2FrameClassFactory
    pass
    class ID3v2TitleFrame(ID3v2Frame):
    __metaclass__ = ID3v2FrameClassFactory
    frame_identifier = "TIT2"
    class ID3v2CommentFrame(ID3v2Frame):
    __metaclass__ = ID3v2FrameClassFactory
    frame_identifier = "COMM"
    title_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('TIT2')
    comment_class = ID3v2FrameClassFactory.get_class_from_frame_identifier('COMM')
    print title_class
    print comment_class
    # Output:
    Creating class ID3v2Frame
    Creating class ID3v2TitleFrame
    Creating class ID3v2CommentFrame
    <class '__main__.ID3v2TitleFrame'>
    <class '__main__.ID3v2CommentFrame'>
    當(dāng)然了,以上的代碼同樣可以用類裝飾器來完成,以下是對(duì)應(yīng)代碼:
    frametype_class_dict = {}
    class ID3v2FrameClass(object):
    def __init__(self, frame_id):
    self.frame_id = frame_id
    def __call__(self, cls):
    print "Decorating class", cls.__name__
    # Here we could add some helper methods or attributes to c
    if self.frame_id:
    frametype_class_dict[self.frame_id] = cls
    return cls
    @staticmethod
    def get_class_from_frame_identifier(frame_identifier):
    return frametype_class_dict.get(frame_identifier)
    @ID3v2FrameClass(None)
    class ID3v2Frame(object):
    pass
    @ID3v2FrameClass("TIT2")
    class ID3v2TitleFrame(ID3v2Frame):
    pass
    @ID3v2FrameClass("COMM")
    class ID3v2CommentFrame(ID3v2Frame):
    pass
    title_class = ID3v2FrameClass.get_class_from_frame_identifier('TIT2')
    comment_class = ID3v2FrameClass.get_class_from_frame_identifier('COMM')
    print title_class
    print comment_class
    Decorating class ID3v2Frame
    Decorating class ID3v2TitleFrame
    Decorating class ID3v2CommentFrame
    <class '__main__.ID3v2TitleFrame'>
    <class '__main__.ID3v2CommentFrame'>
    如你所見,我們可以直接給裝飾器傳遞參數(shù),而元類卻不能。給元類傳遞參數(shù)必須通過屬性。正因如此,這里裝飾器的解決方案更為清晰,同時(shí)也更容易維護(hù)。然而,同時(shí)也需要注意當(dāng)裝飾器被調(diào)用的時(shí)候,類已經(jīng)建立完畢,這意味著此時(shí)就不能夠修改其屬性了。例如,一旦類建立完成,你就不能夠修改__doc__。來看實(shí)際例子:
    >>> def mydecorator(cls):
    ... cls.__doc__ = "Test!"
    ... return cls
    ...
    >>> @mydecorator
    ... class C(object):
    ... """Docstring to be replaced with Test!"""
    ... pass
    ...
    Traceback (most recent call last):
    File "<stdin>", line 2, in <module>
    File "<stdin>", line 2, in mydecorator
    AttributeError: attribute '__doc__' of 'type' objects is not writable
    >>> def mymetaclass(cls, parents, attrs):
    ... attrs['__doc__'] = 'Test!'
    ... return type(cls, parents, attrs)
    ...
    >>> class D(object):
    ... """Docstring to be replaced with Test!"""
    ... __metaclass__ = mymetaclass
    ...
    >>> D.__doc__
    'Test!'
    通過type生成元類
    正如我們所說,最基本的元類就是type并且類通常都是type類型。那么問題很自然來了,type類型本身是一種什么類型呢?答案也是type。這也就是說type就是它自身的元類。雖然聽起來有點(diǎn)詭異,但這在Python解釋器層面而言是可行的。
    type自身就是一個(gè)類,并且我們可以從它繼承出新類。這些生成的類也能作為元類,并且使用它們的類可以得到跟使用type一樣的類型。來看以下的例子:
    >>> class meta(type):
    ... def __new__(cls, class_name, parents, attributes):
    ... print "meta.__new__"
    ... return super(meta, cls).__new__(cls, class_name, parents, attributes)
    ... def __call__(self, *args, **kwargs):
    ... print "meta.__call__"
    ... return super(meta, self).__call__(*args, **kwargs)
    ...
    >>> class C(object):
    ... __metaclass__ = meta
    ...
    meta.__new__
    >>> c = C()
    meta.__call__
    >>> type(C)
    <class '__main__.meta'>
    請(qǐng)注意當(dāng)類創(chuàng)建對(duì)象時(shí),元類的__call__函數(shù)就被調(diào)用,進(jìn)而調(diào)用type.__call__創(chuàng)建對(duì)象。在下一節(jié),我們將把上面的內(nèi)容融合在一起。
    要點(diǎn)集合
    假定一個(gè)類C自己的元類為my_metaclass并被裝飾器my_class_decorator裝飾。并且,假定my_metaclass本身就是一個(gè)類,從type生成。讓我們將上面提到的內(nèi)容融合到一起做一個(gè)總結(jié)來顯示C類以及它的對(duì)象都是怎么被創(chuàng)建的。首先,讓我們來看看代碼:
    class my_metaclass(type):
    def __new__(cls, class_name, parents, attributes):
    print "- my_metaclass.__new__ - Creating class instance of type", cls
    return super(my_metaclass, cls).__new__(cls,
    class_name,
    parents,
    attributes)
    def __init__(self, class_name, parents, attributes):
    print "- my_metaclass.__init__ - Initializing the class instance", self
    super(my_metaclass, self).__init__(self)
    def __call__(self, *args, **kwargs):
    print "- my_metaclass.__call__ - Creating object of type ", self
    return super(my_metaclass, self).__call__(*args, **kwargs)
    def my_class_decorator(cls):
    print "- my_class_decorator - Chance to modify the class", cls
    return cls
    @my_class_decorator
    class C(object):
    __metaclass__ = my_metaclass
    def __new__(cls):
    print "- C.__new__ - Creating object."
    return super(C, cls).__new__(cls)
    def __init__(self):
    print "- C.__init__ - Initializing object."
    c = C()
    print "Object c =", c
    現(xiàn)在,你可以花幾分鐘時(shí)間測(cè)試一下你的理解,并且猜一猜打印輸出的順序。
    首先,讓我們來看看Python的解釋器是如何閱讀這部分代碼的,然后我們會(huì)對(duì)應(yīng)輸出來加深我們的理解。
    1. Python首先看類聲明,準(zhǔn)備三個(gè)傳遞給元類的參數(shù)。這三個(gè)參數(shù)分別為類名(class_name),父類(parent)以及屬性列表(attributs)。
    2. Python會(huì)檢查__metaclass__屬性,如果設(shè)置了此屬性,它將調(diào)用metaclass,傳遞三個(gè)參數(shù),并且返回一個(gè)類。
    3. 在這個(gè)例子中,metaclass自身就是一個(gè)類,所以調(diào)用它的過程類似創(chuàng)建一個(gè)新類。這就意味著my_metaclass.__new__將首先被調(diào)用,輸入四個(gè)參數(shù),這將新建一個(gè)metaclass類的實(shí)例。然后這個(gè)實(shí)例的my_metaclass.__init__將被調(diào)用調(diào)用結(jié)果是作為一個(gè)新的類對(duì)象返回。所以此時(shí)C將被設(shè)置成這個(gè)類對(duì)象。
    4. 接下來Python將查看所有裝飾了此類的裝飾器。在這個(gè)例子中,只有一個(gè)裝飾器。Python將調(diào)用這個(gè)裝飾器,將從元類哪里得到的類傳遞給它作為參數(shù)。然后這個(gè)類將被裝飾器返回的對(duì)象所替代。
    5. 裝飾器返回的類類型與元類設(shè)置的相同。
    6. 當(dāng)類被調(diào)用創(chuàng)建一個(gè)新的對(duì)象實(shí)例時(shí),因?yàn)轭惖念愋褪莔etaclass,因此Python將會(huì)調(diào)用元類的__call__方法。在這個(gè)例子中,my_metaclass.__call__只是簡(jiǎn)單的調(diào)用了type.__call__,目的是創(chuàng)建一個(gè)傳遞給它的類的對(duì)象實(shí)例。
    7. 下一步type.__call__通過C.__new__創(chuàng)建一個(gè)對(duì)象。
    8. 最后type.__call__通過C.__new__返回的結(jié)果運(yùn)行C.__init__。
    9. 返回的對(duì)象已經(jīng)準(zhǔn)備完畢。
    所以基于以上的分析,我們可以看到調(diào)用的順序如下:my_metaclass.__new__首先被調(diào)用,然后是my_metaclass.__init__,然后是my_class_decorator。至此C類已經(jīng)準(zhǔn)備完畢(返回結(jié)果就是C)。當(dāng)我們調(diào)用C來創(chuàng)建一個(gè)對(duì)象的時(shí)候,首先會(huì)調(diào)用my_metaclass.__call__(任何對(duì)象被創(chuàng)建的時(shí)候,Python都首先會(huì)去調(diào)用其類的__call__方法),然后C.__new__將會(huì)被type.__call__調(diào)用(my_metaclass.__call__簡(jiǎn)單調(diào)用了type.__call__),最后是C.__init__被調(diào)用。現(xiàn)在讓我們來看看輸出:
    - my_metaclass.__new__ - Creating class instance of type <class '__main__.my_metaclass'>
    - my_metaclass.__init__ - Initializing the class instance <class '__main__.C'>
    - my_class_decorator - Chance to modify the class <class '__main__.C'>
    - my_metaclass.__call__ - Creating object of type <class '__main__.C'>
    - C.__new__ - Creating object.
    - C.__init__ - Initializing object.
    Object c = <__main__.C object at 0x1043feb90> <class '__main__.C'>
    關(guān)于元類多說幾句
    元類,一門強(qiáng)大而晦澀的技法。在GitHub上搜索__metaclass__得到的結(jié)果多半是指向”cookbook”或其他Python教學(xué)材料的鏈接。一些測(cè)試用例(諸如Jython中的一些測(cè)試用例),或是其他一些寫有__metaclass__ = type的地方只是為了確保新類被正常使用了。坦白地說,這些用例都沒有真正地使用元類。過濾了下結(jié)果,我只能找到兩個(gè)地方真正使用了元類:ABCMeta和djangoplugins。
    ABCMeta是一個(gè)允許注冊(cè)抽象基類的元類。如果想了解多些請(qǐng)查看其官方文檔,本文將不會(huì)討論它。
    對(duì)于djangoplugins而言,基本的思想是基于這篇文章article on a simple plugin framework for Python,使用元類是為了創(chuàng)建一個(gè)插件掛載系統(tǒng)。我并沒有對(duì)其有深入的研究,不過我感覺這個(gè)功能可以使用裝飾器來實(shí)現(xiàn)。如果你有相關(guān)的想法請(qǐng)?jiān)?本文后留言。
    總結(jié)筆記
    通過理解元類能夠幫助我們更深入的理解Python中類和對(duì)象的行為,現(xiàn)實(shí)中使用它們的情況可能比文中的例子要復(fù)雜得多。大部分元類完成的功能都可以使用裝飾器來實(shí)現(xiàn)。所以當(dāng)你的第一直覺是使用元類來解決你的問題,那么請(qǐng)你停下來先想想這是否必要。如果不是非要使用元類,那么請(qǐng)三思而行。這會(huì)使你的代碼更易懂,更易調(diào)試和維護(hù)。