分享下自己對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. 你可以自由的、動態的修改/增加/刪除 類的或者實例中的方法或者屬性
- #!/usr/bin/python
- #coding :utf-8
- def ma(cls):
- print 'method a'
- def mb(cls):
- print 'method b'
- method_dict = {
- 'ma': ma,
- 'mb': mb,
- }
- class DynamicMethod(type):
- def __new__(cls, name, bases, dct):
- if name[:3] == 'Abc':
- dct.update(method_dict)
- return type.__new__(cls, name, bases, dct)
- def __init__(cls, name, bases, dct):
- super(DynamicMethod, cls).__init__(name, bases, dct)
- class AbcTest(object):
- __metaclass__ = DynamicMethod
- def mc(self, x):
- print x * 3
- class NotAbc(object):
- __metaclass__ = DynamicMethod
- def md(self, x):
- print x * 3
- def main():
- a = AbcTest()
- a.mc(3)
- a.ma()
- print dir(a)
- b = NotAbc()
- print dir(b)
- if __name__ == '__main__':
- main()
通過DynamicMethod這個metaclass的原型,我們可以在那些指定了__metaclass__屬性位DynamicMethod的類里面,
根據類名字,如果是以'Abc'開頭的就給它加上ma和mb的方法(這里的條件只是一種簡單的例子假設了,實際應用上
可能更復雜),如果不是'Abc'開頭的類就不加. 這樣就可以打到動態添加方法的效果了。其實,你也可以將需要動態
添加或者修改的方法改到__new__里面,因為python是支持在方法里面再定義方法的. 通過這個例子,其實可以看到
只要我們能操作__new__方法里面的其中一個參數attrs,就可以動態添加東西了。
2. 批量的對某些方法使用decorator,而不需要每次都在方法的上面加入@decorator_func
這個其實有應用場景的,就是比如我們cgi程序里面,很多需要驗證登錄或者是否有權限的,只有驗證通過之后才
可以操作。那么如果你有很多個操作都需要這樣做,我們一般情況下可以手動在每個方法的前頭用@login_required
類似這樣的方式。那如果學習了metaclass的使用的話,這次你也可以這樣做:
- #!/usr/bin/python
- #coding :utf-8
- from types import FunctionType
- def login_required(func):
- print 'login check logic here'
- return func
- class LoginDecorator(type):
- def __new__(cls, name, bases, dct):
- for name, value in dct.iteritems():
- if name not in ('__metaclass__', '__init__', '__module__') and\
- type(value) == FunctionType:
- value = login_required(value)
- dct[name] = value
- return type.__new__(cls, name, bases, dct)
- class Operation(object):
- __metaclass__ = LoginDecorator
- def delete(self, x):
- print 'deleted %s' % str(x)
- def main():
- op = Operation()
- op.delete('test')
- if __name__ == '__main__':
- main()
這樣子你就可以不用在delete函數上面寫@login_required, 也能達到decorator的效果了。不過可讀性就差點了。
3. 當引入第三方庫的時候,如果該庫某些類需要patch的時候可以用metaclass
- #!/usr/bin/python
- #coding :utf-8
- def monkey_patch(name, bases, dct):
- assert len(bases) == 1
- base = bases[0]
- for name, value in dct.iteritems():
- if name not in ('__module__', '__metaclass__'):
- setattr(base, name, value)
- return base
- class A(object):
- def a(self):
- print 'i am A object'
- class PatchA(A):
- __metaclass__ = monkey_patch
- def patcha_method(self):
- print 'this is a method patched for class A'
- def main():
- pa = PatchA()
- pa.patcha_method()
- pa.a()
- print dir(pa)
- print dir(PatchA)
- if __name__ == '__main__':
- 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. 以上都是個人的觀點和總結而已,歡迎拍磚。