1. 程式人生 > >python元類解析

python元類解析

分享下自己對python的metaclass的知識。

一 你可以從這裡獲取什麼?

1. 也許你在閱讀別人的程式碼的時候碰到過metaclass,那你可以參考這裡的介紹。

2. 或許你需要設計一些底層的庫,也許metaclass能幫你簡化你的設計(也有可能複雜化:)

3. 也許你在瞭解metaclass的相關知識之後,你對python的類的一些機制會更瞭解。

4. more......

二 metaclass的作用是什麼?(感性認識)

 metaclass能有什麼用處,先來個感性的認識:

1. 你可以自由的、動態的修改/增加/刪除 類的或者例項中的方法或者屬性

2. 批量的對某些方法使用decorator,而不需要每次都在方法的上面加入@decorator_func

3. 當引入第三方庫的時候,如果該庫某些類需要patch的時候可以用metaclass

4. 可以用於序列化(參見yaml這個庫的實現,我沒怎麼仔細看)

5. 提供介面註冊,介面格式檢查等

6. 自動委託(auto delegate)

7. more...

三 metaclass的相關知識

1. what is metaclass?

1.1 在wiki上面,metaclass是這樣定義的:In object-oriented programming, 

a metaclass is a class whose instances are classes. 

Just as an ordinary class defines the behavior of certain objects, 

a metaclass defines the behavior of certain classes and their instances.

也就是說metaclass的例項化結果是類,而class例項化的結果是instance。我是這麼理解的:

metaclass是類似建立類的模板,所有的類都是通過他來create的(呼叫__new__),這使得你可以自由的控制

建立類的那個過程,實現你所需要的功能。

1.2 metaclass基礎

* 一般情況下, 如果你要用類來實現metaclass的話,該類需要繼承於type,而且通常會重寫type的__new__方法來控制建立過程。

當然你也可以用函式的方式(下文會講)

* 在metaclass裡面定義的方法會成為類的方法,可以直接通過類名來呼叫

2. 如何使用metaclass

2.1 用類的形式

2.1.1 類繼承於type, 例如: class Meta(type):pass

2.1.2 將需要使用metaclass來構建class的類的__metaclass__屬性(不需要顯示宣告,直接有的了)賦值為Meta(繼承於type的類)

2.2 用函式的形式

2.2.1 構建一個函式,例如叫metaclass_new, 需要3個引數:name, bases, attrs,

name: 類的名字

bases: 基類,通常是tuple型別

attrs: dict型別,就是類的屬性或者函式

2.2.2 將需要使用metaclass來構建class的類的__metaclass__屬性(不需要顯示宣告,直接有的了)賦值為函式metaclas_new

3 metaclass 原理

3.1 basic

metaclass的原理其實是這樣的:當定義好類之後,建立類的時候其實是呼叫了type的__new__方法為這個類分配記憶體空間,建立

好了之後再呼叫type的__init__方法初始化(做一些賦值等)。所以metaclass的所有magic其實就在於這個__new__方法裡面了。

說說這個方法:__new__(cls, name, bases, attrs)

cls: 將要建立的類,類似與self,但是self指向的是instance,而這裡cls指向的是class

name: 類的名字,也就是我們通常用類名.__name__獲取的。

bases: 基類

attrs: 屬性的dict。dict的內容可以是變數(類屬性),也可以是函式(類方法)。

所以在建立類的過程,我們可以在這個函式裡面修改name,bases,attrs的值來自由的達到我們的功能。這裡常用的配合方法是

getattr和setattr(just an advice)

3.2 查詢順序

再說說關於__metaclass__這個屬性。這個屬性的說明是這樣的:

This variable can be any callable accepting arguments for name, bases, and dict. Upon class creation, the callable is used instead of the built-in type(). New in version 2.2.(所以有了上面介紹的分別用類或者函式的方法)

The appropriate metaclass is determined by the following precedence rules: 

If dict['__metaclass__'] exists, it is used. 

Otherwise, if there is at least one base class, its metaclass is used (this looks for a __class__ attribute first and if not found, uses its type). 

Otherwise, if a global variable named __metaclass__ exists, it is used. 

Otherwise, the old-style, classic metaclass (types.ClassType) is used. 

這個查詢順序也比較好懂,而且利用這個順序的話,如果是old-style類的話,可以在某個需要的模組裡面指定全域性變數

__metaclass__ = type就能把所有的old-style 變成 new-style的類了。(這是其中一種trick)

四 例子

針對第二點說的metaclass的作用,順序來給些例子:

1. 你可以自由的、動態的修改/增加/刪除 類的或者例項中的方法或者屬性

Python程式碼  收藏程式碼
  1. #!/usr/bin/python  
  2. #coding :utf-8  
  3. def ma(cls):  
  4.     print 'method a'  
  5. def mb(cls):  
  6.     print 'method b'  
  7. method_dict = {  
  8.     'ma': ma,  
  9.     'mb': mb,  
  10. }  
  11. class DynamicMethod(type):  
  12.     def __new__(cls, name, bases, dct):  
  13.         if name[:3] == 'Abc':  
  14.             dct.update(method_dict)  
  15.         return type.__new__(cls, name, bases, dct)  
  16.     def __init__(cls, name, bases, dct):  
  17.         super(DynamicMethod, cls).__init__(name, bases, dct)  
  18. class AbcTest(object):  
  19.     __metaclass__ = DynamicMethod  
  20.     def mc(self, x):  
  21.         print x * 3  
  22. class NotAbc(object):  
  23.     __metaclass__ = DynamicMethod  
  24.     def md(self, x):  
  25.         print x * 3  
  26. def main():  
  27.     a = AbcTest()  
  28.     a.mc(3)  
  29.     a.ma()  
  30.     print dir(a)  
  31.     b = NotAbc()  
  32.     print dir(b)  
  33. if __name__ == '__main__':  
  34.     main()  

通過DynamicMethod這個metaclass的原型,我們可以在那些指定了__metaclass__屬性位DynamicMethod的類裡面,

根據類名字,如果是以'Abc'開頭的就給它加上ma和mb的方法(這裡的條件只是一種簡單的例子假設了,實際應用上

可能更復雜),如果不是'Abc'開頭的類就不加. 這樣就可以打到動態新增方法的效果了。其實,你也可以將需要動態

新增或者修改的方法改到__new__裡面,因為python是支援在方法裡面再定義方法的. 通過這個例子,其實可以看到

只要我們能操作__new__方法裡面的其中一個引數attrs,就可以動態新增東西了。

2. 批量的對某些方法使用decorator,而不需要每次都在方法的上面加入@decorator_func

這個其實有應用場景的,就是比如我們cgi程式裡面,很多需要驗證登入或者是否有許可權的,只有驗證通過之後才

可以操作。那麼如果你有很多個操作都需要這樣做,我們一般情況下可以手動在每個方法的前頭用@login_required

類似這樣的方式。那如果學習了metaclass的使用的話,這次你也可以這樣做:

Python程式碼  收藏程式碼
  1. #!/usr/bin/python  
  2. #coding :utf-8  
  3. from types import FunctionType  
  4. def login_required(func):  
  5.     print 'login check logic here'  
  6.     return func  
  7. class LoginDecorator(type):  
  8.     def __new__(cls, name, bases, dct):  
  9.         for name, value in dct.iteritems():  
  10.             if name not in ('__metaclass__''__init__''__module__'and\  
  11.                 type(value) == FunctionType:  
  12.                 value = login_required(value)  
  13.             dct[name] = value  
  14.         return type.__new__(cls, name, bases, dct)  
  15. class Operation(object):  
  16.     __metaclass__ = LoginDecorator  
  17.     def delete(self, x):  
  18.         print 'deleted %s' % str(x)  
  19. def main():  
  20.     op = Operation()  
  21.     op.delete('test')  
  22. if __name__ == '__main__':  
  23.     main()  

這樣子你就可以不用在delete函式上面寫@login_required, 也能達到decorator的效果了。不過可讀性就差點了。

3. 當引入第三方庫的時候,如果該庫某些類需要patch的時候可以用metaclass

Python程式碼  收藏程式碼
  1. #!/usr/bin/python  
  2. #coding :utf-8  
  3. def monkey_patch(name, bases, dct):  
  4.     assert len(bases) == 1  
  5.     base = bases[0]  
  6.     for name, value in dct.iteritems():  
  7.         if name not in ('__module__''__metaclass__'):  
  8.             setattr(base, name, value)  
  9.     return base  
  10. class A(object):  
  11.     def a(self):  
  12.         print 'i am A object'  
  13. class PatchA(A):  
  14.     __metaclass__ = monkey_patch  
  15.     def patcha_method(self):  
  16.         print 'this is a method patched for class A'  
  17. def main():  
  18.     pa = PatchA()  
  19.     pa.patcha_method()  
  20.     pa.a()  
  21.     print dir(pa)  
  22.     print dir(PatchA)  
  23. if __name__ == '__main__':  
  24.     main()  

5. 提供介面註冊,介面格式檢查等, 這個功能可以參考這篇文章:

6. 自動委託(auto delegate)

以下是網上的例子:

http://marlonyao.iteye.com/blog/762156

五 總結

1. metaclass的使用原則:

If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why). --Tim Peters

也就是說如果你不知道能用metaclass來幹什麼的話,你儘量不要用,因為通常metaclass的程式碼會增加程式碼的複雜度,

降低程式碼的可讀性。所以你必需權衡metaclass的利弊。

2. metaclass的優勢在於它的動態性和可描述性(比如上面例子中的self.delegate.__getitem__(i)這樣的程式碼,它

可以用另外的函式程式碼生成,而無需每次手動編寫), 它能把類的動態性擴充套件到極致。

六 補充

以下是同事們的很好的補充:

張同學:

1.metaclass屬於超程式設計(metaprogramming)的範疇,所謂超程式設計就是讓程式來寫

(generate/modify)程式,這通常依賴於語言及其執行時系統的動態特性(其實像C

這樣的語言也可以進行超程式設計)。正如樓主所說,超程式設計的一個用途就是“可以用另

外的函式程式碼生成,而無需每次手動編寫“,在python中我們可以做得更多。

2.對於python而言,metaclass使程式設計師可以干涉class的建立過程,並可以在任何

時候修改這樣的class(包括修改metaclass),由於class的意義是為instance集合

持有“方法”,所以修改了一個class就等於修改了所有這些instance的行為,這是

很好的service。

3.注意metaclass的__new__和__init__的區別。

class DynamicMethod(type):

    def __new__(cls, name, bases, dct):  # cls=DynamicMethod

    def __init__(cls, name, bases, dct): # cls=你建立的class物件

這意味著在__new__中我們通常只是修改dct,但是在__init__中,我們可以直接修

改建立好的類,所以我認為這兩個介面的主要區別有2點:1)呼叫時機不同(用處請

發散思維);2)__init__比__new__更有用,我在實際專案中一般都是用__init__的。

4.在python中我們為什麼要修改class?那當然是為了改變它的行為,或者為了創

建出獨一無二的類。實際中常常需要為class動態新增方法。比如一個數據庫表A有

欄位name, address等,表B有name, phone等,你希望A的模型類有

find_by_address、find_by_name_and_address等方法,希望B的模型類有

find_by_name、find_by_phone等方法,但是又不想手寫這些方法(其實不可能手

寫,因為這種組合太多了),這時你可以在A、B共同的metaclass中定義一個自動添

加方法的子程式,當然你也可以重寫__getattr__之類的介面來hook所有

find_by_XXX函式呼叫,這就是時間換空間了,想象你要執行find_by_XXX一百萬

次。也可以比較一下在c++/java中如何應對這種需求。

5.python的成功之處在於它無處不在的namespace(就是那個__dict__,其意義可以

參考SICP第一章的environment模型,對計算理論感興趣的也可以用lambda演算來

解釋),而且函式又是first class物件,又強化了interface的使用。我們知道了

metaclass->class->instance的關係,又知道了物件的方法是放在類裡的(請詳細

考察python查詢一個方法的流程),那麼用python實現各種設計模式就比較簡單了。

6.metaclass不會使程式變晦澀,前提是瞭解了metaclass的固有存在,許多教程的

問題就在於它沒有告訴讀者metaclass的存在,而是藉助某些其他語言(比如c++)的

類模型來講解python。在ruby的型別系統中metaclass是無限的,metaclass也有自

己的metaclass(你可以稱之為metametaclass、metametametaclass等等),ruby

善於實現DSL和語法分析器也部分得益於此。

嶽同學:

不能說__init__比__new__更有用吧。我覺得要看場合。畢竟__new__能做到比

__init__更多的事情。比如有時候想改生成的型別名字,或者改型別的父類。:)

不過的確大多數場合用__init__就夠用了。+1

在__init__中控制類生成的過程有一點要注意:在__init__()的最後一個參

數(attrs)中,對於類中定義的函式型別的屬性,比如:

def abc(self):

     pass

仍然具有以下的key->value形式:

"abc":<function object>

但是在生成的類中,"abc"對應的屬性已經從一個function變成了一個unbind

method:

self.abc --> unbind method

不過實際使用中影響不大。

七 其他

1. 哪些專案有用metaclass:

據我所知就是django中的資料庫部分的很多都使用metaclass來實現可描述性的

還有google app engine的程式碼裡面也有使用

yaml中的序列化部分程式碼也有使用

more...

2. 參考資料:

3. 以上都是個人的觀點和總結而已,歡迎拍磚。