淺析python的metaclass


 

分享下自己對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.   
  4.   
  5. def ma(cls):  
  6.     print 'method a'  
  7.   
  8. def mb(cls):  
  9.     print 'method b'  
  10.   
  11. method_dict = {  
  12.     'ma': ma,  
  13.     'mb': mb,  
  14. }  
  15.   
  16. class DynamicMethod(type):  
  17.     def __new__(cls, name, bases, dct):  
  18.         if name[:3] == 'Abc':  
  19.             dct.update(method_dict)  
  20.         return type.__new__(cls, name, bases, dct)  
  21.   
  22.     def __init__(cls, name, bases, dct):  
  23.         super(DynamicMethod, cls).__init__(name, bases, dct)  
  24.   
  25.   
  26. class AbcTest(object):  
  27.     __metaclass__ = DynamicMethod  
  28.     def mc(self, x):  
  29.         print x * 3  
  30.   
  31. class NotAbc(object):  
  32.     __metaclass__ = DynamicMethod  
  33.     def md(self, x):  
  34.         print x * 3  
  35.   
  36. def main():  
  37.     a = AbcTest()  
  38.     a.mc(3)  
  39.     a.ma()  
  40.     print dir(a)  
  41.   
  42.     b = NotAbc()  
  43.     print dir(b)  
  44.   
  45. if __name__ == '__main__':  
  46.     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.   
  5. def login_required(func):  
  6.     print 'login check logic here'  
  7.     return func  
  8.   
  9.   
  10. class LoginDecorator(type):  
  11.     def __new__(cls, name, bases, dct):  
  12.         for name, value in dct.iteritems():  
  13.             if name not in ('__metaclass__', '__init__', '__module__') and\  
  14.                 type(value) == FunctionType:  
  15.                 value = login_required(value)  
  16.   
  17.             dct[name] = value  
  18.         return type.__new__(cls, name, bases, dct)  
  19.   
  20.   
  21. class Operation(object):  
  22.     __metaclass__ = LoginDecorator  
  23.   
  24.     def delete(self, x):  
  25.         print 'deleted %s' % str(x)  
  26.   
  27.   
  28. def main():  
  29.     op = Operation()  
  30.     op.delete('test')  
  31.   
  32. if __name__ == '__main__':  
  33.     main()  

 

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

 

 

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

 

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

 

 

5. 提供接口注冊,接口格式檢查等, 這個功能可以參考這篇文章:

http://mikeconley.ca/blog/2010/05/04/python-metaclasses-in-review-board/

 

 

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. 參考資料:

* Metaclass Programming In Python [http://gnosis.cx/publish/programming/metaclass_1.html]

* Python中用MetaClass實現委托、不可變集合 [http://marlonyao.iteye.com/blog/762156]

* Metaclass [http://en.wikipedia.org/wiki/Metaclasses#Python_example]

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM