類和對象
類和函數(shù)一樣都是Python中的對象。當一個類定義完成之后,Python將創(chuàng)建一個“類對象”并將其賦值給一個同名變量。類是type類型的對象(是不是有點拗口?)。
類對象是可調用的(callable,實現(xiàn)了 __call__方法),并且調用它能夠創(chuàng)建類的對象。你可以將類當做其他對象那么處理。例如,你能夠給它們的屬性賦值,你能夠將它們賦值給一個變量,你可以在任何可調用對象能夠用的地方使用它們,比如在一個map中。事實上當你在使用map(str, [1,2,3])的時候,是將一個整數(shù)類型的list轉換為字符串類型的list,因為str是一個類。可以看看下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
>>> class C( object ): ... def __init__( self , s): ... print s ... >>> myclass = C >>> type (C) < type 'type' > >>> type (myclass) < type 'type' > >>> myclass( 2 ) 2 <__main__.C object at 0x10e2bea50 > >>> map (myclass, [ 1 , 2 , 3 ]) 1 2 3 [<__main__.C object at 0x10e2be9d0 >, <__main__.C object at 0x10e2bead0 >, <__main__.C object at 0x10e2beb10 >] >>> map (C, [ 1 , 2 , 3 ]) 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”關鍵字不像其他語言(例如C++)那樣必須出現(xiàn)在代碼main scope中。在Python中,它能夠在一個函數(shù)中嵌套出現(xiàn),舉個例子,我們能夠這樣在函數(shù)運行的過程中動態(tài)的創(chuàng)建類。看代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
>>> 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 >,) |
請注意,在這里通過make_class創(chuàng)建的兩個類是不同的對象,因此通過它們創(chuàng)建的對象就不屬于同一個類型。正如我們在裝飾器中做的那樣,我們在類被創(chuàng)建之后手動設置了類名。同樣也請注意所創(chuàng)建類的print_class_name方法在一個closure cell中捕捉到了類的closure和class_name。如果你對closure的概念還不是很清楚,那么最好去看看前篇,復習一下closures和decorators相關的內容。
Metaclasses
如果類是能夠制造對象的對象,那制造類的對象又該叫做什么呢(相信我,這并不是一個先有雞還是先有蛋的問題)?答案是元類(Metaclasses)。大部分常見的基礎元類都是type。當輸入一個參數(shù)時,type將簡單的返回輸入對象的類型,這就不涉及元類。然而當輸入三個參數(shù)時,type將扮演元類的角色,基于輸入參數(shù)創(chuàng)建一個類并返回。輸入參數(shù)相當簡單:類名,父類及其參數(shù)的字典。后面兩者可以為空,來看一個例子:
1
2
3
4
5
6
|
>>> MyClass = type ( "MyClass" , ( object ,), { "my_attribute" : 0 }) >>> type (MyClass) < type 'type' > >>> o = MyClass() >>> o.my_attribute 0 |
特別注意第二個參數(shù)是一個tuple(語法看起來很奇怪,以逗號結尾)。如果你需要在類中安排一個方法,那么創(chuàng)建一個函數(shù)并且將其以屬性的方式傳遞作為第三個參數(shù),像這樣:
1
2
3
4
5
6
7
8
9
|
>>> 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 >> |
我們可以通過一個可調用對象(函數(shù)或是類)來自定義元類,這個對象需要三個輸入參數(shù)并返回一個對象。這樣一個元類在一個類上實現(xiàn)只要定義了它的__metaclass__屬性。第一個例子,讓我們做一些有趣的事情看看我們能夠用元類做些什么:
1
2
3
4
5
6
7
8
9
10
|
>>> def mymetaclass(name, parents, attributes): ... return "Hello" ... >>> class C( object ): ... __metaclass__ = mymetaclass ... >>> print C Hello >>> type (C) < type 'str' > |
請注意以上的代碼,C只是簡單地將一個變量引用指向了字符串“Hello”。當然了,沒人會在實際中寫這樣的代碼,這只是為了演示元類的用法而舉的一個簡單例子。接下來我們來做一些更有用的操作。在本系列的第二部分我們曾看到如何使用裝飾器類來記錄目標類每個方法的輸出,現(xiàn)在我們來做同樣的事情,不過這一次我們使用元類。我們借用之前的裝飾器定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
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 |
如你所見,類裝飾器與元類有著很多共同點。事實上,任何能夠用類裝飾器完成的功能都能夠用元類來實現(xiàn)。類裝飾器有著很簡單的語法結構易于閱讀,所以提倡使用。但就元類而言,它能夠做的更多,因為它在類被創(chuàng)建之前就運行了,而類裝飾器則是在類創(chuàng)建之后才運行的。記住這點,讓我們來同時運行一下兩者,請注意運行的先后順序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 . |
元類的一個實際用例
讓我們來考慮一個更有用的實例。假設我們正在構思一個類集合來處理MP3音樂文件中使用到的ID3v2標簽Wikipedia。簡而言之,標簽由幀(frames)組成,而每幀通過一個四字符的識別碼(identifier)進行標記。舉個例子,TOPE標識了原作者幀,TOAL標識了原專輯名稱等。如果我們希望為每個幀類型寫一個單獨的類,并且允許ID3v2標簽庫用戶自定義他們自己的幀類。那么我們可以使用元類來實現(xiàn)一個類工廠模式,具體實現(xiàn)方式可以這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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' > |
當然了,以上的代碼同樣可以用類裝飾器來完成,以下是對應代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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ù)必須通過屬性。正因如此,這里裝飾器的解決方案更為清晰,同時也更容易維護。然而,同時也需要注意當裝飾器被調用的時候,類已經建立完畢,這意味著此時就不能夠修改其屬性了。例如,一旦類建立完成,你就不能夠修改__doc__。來看實際例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
>>> 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就是它自身的元類。雖然聽起來有點詭異,但這在Python解釋器層面而言是可行的。
type自身就是一個類,并且我們可以從它繼承出新類。這些生成的類也能作為元類,并且使用它們的類可以得到跟使用type一樣的類型。來看以下的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
>>> 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' > |
請注意當類創(chuàng)建對象時,元類的__call__函數(shù)就被調用,進而調用type.__call__創(chuàng)建對象。在下一節(jié),我們將把上面的內容融合在一起。
要點集合
假定一個類C自己的元類為my_metaclass并被裝飾器my_class_decorator裝飾。并且,假定my_metaclass本身就是一個類,從type生成。讓我們將上面提到的內容融合到一起做一個總結來顯示C類以及它的對象都是怎么被創(chuàng)建的。首先,讓我們來看看代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
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)在,你可以花幾分鐘時間測試一下你的理解,并且猜一猜打印輸出的順序。
首先,讓我們來看看Python的解釋器是如何閱讀這部分代碼的,然后我們會對應輸出來加深我們的理解。
1. Python首先看類聲明,準備三個傳遞給元類的參數(shù)。這三個參數(shù)分別為類名(class_name),父類(parent)以及屬性列表(attributs)。
2. Python會檢查__metaclass__屬性,如果設置了此屬性,它將調用metaclass,傳遞三個參數(shù),并且返回一個類。
3. 在這個例子中,metaclass自身就是一個類,所以調用它的過程類似創(chuàng)建一個新類。這就意味著my_metaclass.__new__將首先被調用,輸入四個參數(shù),這將新建一個metaclass類的實例。然后這個實例的my_metaclass.__init__將被調用調用結果是作為一個新的類對象返回。所以此時C將被設置成這個類對象。
4. 接下來Python將查看所有裝飾了此類的裝飾器。在這個例子中,只有一個裝飾器。Python將調用這個裝飾器,將從元類哪里得到的類傳遞給它作為參數(shù)。然后這個類將被裝飾器返回的對象所替代。
5. 裝飾器返回的類類型與元類設置的相同。
6. 當類被調用創(chuàng)建一個新的對象實例時,因為類的類型是metaclass,因此Python將會調用元類的__call__方法。在這個例子中,my_metaclass.__call__只是簡單的調用了type.__call__,目的是創(chuàng)建一個傳遞給它的類的對象實例。
7. 下一步type.__call__通過C.__new__創(chuàng)建一個對象。
8. 最后type.__call__通過C.__new__返回的結果運行C.__init__。
9. 返回的對象已經準備完畢。
所以基于以上的分析,我們可以看到調用的順序如下:my_metaclass.__new__首先被調用,然后是my_metaclass.__init__,然后是my_class_decorator。至此C類已經準備完畢(返回結果就是C)。當我們調用C來創(chuàng)建一個對象的時候,首先會調用my_metaclass.__call__(任何對象被創(chuàng)建的時候,Python都首先會去調用其類的__call__方法),然后C.__new__將會被type.__call__調用(my_metaclass.__call__簡單調用了type.__call__),最后是C.__init__被調用。現(xiàn)在讓我們來看看輸出:
1
2
3
4
5
6
7
|
- 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' > |
關于元類多說幾句
元類,一門強大而晦澀的技法。在GitHub上搜索__metaclass__得到的結果多半是指向”cookbook”或其他Python教學材料的鏈接。一些測試用例(諸如Jython中的一些測試用例),或是其他一些寫有__metaclass__ = type的地方只是為了確保新類被正常使用了。坦白地說,這些用例都沒有真正地使用元類。過濾了下結果,我只能找到兩個地方真正使用了元類:ABCMeta和djangoplugins。
ABCMeta是一個允許注冊抽象基類的元類。如果想了解多些請查看其官方文檔,本文將不會討論它。
對于djangoplugins而言,基本的思想是基于這篇文章article on a simple plugin framework for Python,使用元類是為了創(chuàng)建一個插件掛載系統(tǒng)。我并沒有對其有深入的研究,不過我感覺這個功能可以使用裝飾器來實現(xiàn)。如果你有相關的想法請在 本文后留言。
總結筆記
通過理解元類能夠幫助我們更深入的理解Python中類和對象的行為,現(xiàn)實中使用它們的情況可能比文中的例子要復雜得多。大部分元類完成的功能都可以使用裝飾器來實現(xiàn)。所以當你的第一直覺是使用元類來解決你的問題,那么請你停下來先想想這是否必要。如果不是非要使用元類,那么請三思而行。這會使你的代碼更易懂,更易調試和維護。