(原發於個人blog:Python: metaclass小記 )php
友情提示:本文不必定適合閱讀,若是執意要讀,請備好暈車藥。python
"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."數據庫
-- Tim Peters緩存
這句話聽起來就很誘人,曾經試圖去理解它,可是由於沒有實際的需求,就由於燒腦子而放棄了。不妨摘錄一段Python document裏關於metaclass的概述,簡直就是繞口令:函數
Terminology-wise, a metaclass is simply "the class of a class". Any class whose instances are themselves classes, is a metaclass. When we talk about an instance that's not a class, the instance's metaclass is the class of its class: by definition, x's metaclass is x.__class__.__class__. But when we talk about a class C, we often refer to its metaclass when we mean C.__class__ (not C.__class__.__class__, which would be a meta-metaclass; there's not much use for those although we don't rule them out).oop
昨天心血來潮想寫一個帶class initializer的class,發現繞不過metaclass了,因而又翻出來看。ui
實際上是要理解metaclass的本質,無非是要時刻牢記兩點:1. Python中一切皆對象; 2. class也是一個對象,它的class就是metaclass。編碼
舉例來講:設計
class A(object): pass a = A() print (a, id(a), type(a))
(<__main__.A object at 0xb183d0>, 11633616, <class '__main__.A'>)3d
print (A, id(A), type(A))
(<class '__main__.A'>, 11991040, <type 'type'>)
print (type, id(type), type(type))
(<type 'type'>, 1891232, <type 'type'>)
其中第一個print很好理解:a是一個A的實例,有本身的id(其實就是內存地址)、a的class是A。
第二個print就有點燒腦子了:A是一個class,也有本身的id(由於A也是一個對象,雖然print出來的時候沒有明確說),A的class是type。
而第三個就暈乎了:type是一個type,也有本身的id(由於type也是一個對象),type的class是type,也就是它本身。
再回想上面提到的兩點:A是一個對象,它的class是metaclass。也就是說 type 是一個metaclass,而A類是type類的一個對象。
唉,原本想好好解釋的,沒想到仍是說成繞口令了。算了,反正我懂了,繼續。
沒有仔細瞭解type是什麼的同窗可能會覺得type是一個函數:type(X)用於返回X的類對象。
然而並不徹底是這樣的:在python裏,X(args)多是調用一個函數,也多是在實例化一個X的對象——而很不幸地,type(X)其實是介於兩者之間的一個調用:雖然type是一個class,可是它的__call__方法是存在的,因而python把它當成一個函數來調用,實際調用到了源碼中的type_call;type_call調用了type.__new__試圖初始化一個type類的實例,然而type.__new__(位於源碼中的type_new函數)發現臥槽竟然只有一個參數,因而就返回了這個參數的type(源碼是這麼寫的:"return (PyObject *) Py_TYPE(x);",並無生成新的對象)。也就是說,原本是個函數調用,裏面倒是要初始化一個對象,然而最後返回的卻不是初始化的對象!尼瑪那個特殊狀況爲毛不放到函數調用裏面啊,開發者腦抽了嗎!
感到腦抽的同窗能夠暫時忽略上面那段話,跟本文沒太大關係。繼續。
實際上type是在builtin模塊中定義,指向源碼中PyType_Type對象的一個引用:
//位於Python/bltinmodule.c PyObject * _PyBuiltin_Init(void) { ... SETBUILTIN("type", &PyType_Type); ... }
這個PyType_Type又是個什麼鬼?好吧,繼續貼源碼
//位於Objects/typeobject.c PyTypeObject PyType_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "type", /* tp_name */ ... type_init, /* tp_init */ 0, /* tp_alloc */ type_new, /* tp_new */ ... };
注意3點:
PyType_Type,也就是python裏的type,是在源碼中生成的一個對象;這個對象的類型是PyTypeObject,因此它剛好又是一個類,至於你信不信,反正我信了。後面我把它叫作類對象,注意:不是類的對象,而是類自己是一個對象。
PyVarObject_HEAD_INIT遞歸引用了本身(PyType_Type)做爲它的type(在源碼中,指定某個對象的type爲X,就是指定了它在python環境中的class爲X),因此前面第三個print中能夠看到,type(type) == type(哈哈哈,寫繞口令真好玩)
在PyType_Type的定義指定了 tp_init = type_init 和 tp_new = type_new 這兩個屬性值。這是兩個函數,也位於源碼中的Object/typeobject.c。
關於第3點,在Python document中關於__new__方法的說明裏有詳細的介紹,這裏簡單總結一下:在new一個對象的時候,只會調用這個class的__new__方法,它須要生成一個object、調用這個對象的__init__方法對它進行初始化,而後返回這個對象。
好吧,我發現不得不把簡單總結展開,不然確實說不清楚。
這是一個頗有意思的設計:把實例化的流程暴露給碼農,意味着碼農能夠在對象的生成前、生成後返回前兩個環節對這個對象進行修改(【甚至】
在__new__方法中生成並返回的對象並無強制要求必定是該class的實例!不過在document裏建議,若是要覆蓋__new__方法,那麼【應當】
返回這個class的父類的__new__方法返回的對象)。這裏還有一個很是tricky的地方:雖然沒有明確指定,可是__new__方法被硬編碼爲一個staticmethod(有興趣的話能夠去翻type_new函數),它的第一個參數是須要被實例化的class,其他參數則是須要傳給__init__的參數。
提及來很是枯燥,仍是舉一個例子吧,就用document裏給出的Singleton:
class Singleton(object): def __new__(cls, *args, **kwargs): it = cls.__dict__.get("__it__") if it is not None: return it cls.__it__ = it = object.__new__(cls) #注意 it.__init__(*args, **kwargs) return it def __init__(self, *args, **kwargs): pass class DbConnection(Singleton): def __init__(self, db_config): self._connection = AnyHowToConnectBy(db_config) conn = new DbConnection(db_config)
代碼並不複雜,可是可能有點玄乎,須要理解一下那個cls參數,前面說了,它是須要被實例化的class,也就是說,最後一行實際執行的是:
DbConnection.__new__(DbConnection, db_config)
而DbConnection的__new__方法直接繼承於Singleton, 因此實際調用的是
Singleton.__new__(DbConnection, db_config)
主要注意的地方,在上面這段代碼的第六行,Singleton是繼承於object(這裏特指python中的那個object對象),所以調用了object.__new__(DbConnection)
來生成一個對象,生成過程位於C源碼中的object_new函數(Objects/typeobject.c),它會將新生成對象的type指定爲DbConnection,而後直接返回。
Singleton.__new__在拿到了生成的DbConnection實例之後,將它保存在了DbConnection類的__it__屬性中,而後對該實例進行初始化,最後返回。
能夠看到,任何繼承於Singleton類的子類,只要不覆蓋其__new__方法,每一個類永遠只會被實例化一次。
好了,第2點暫告一段落,接下來回歸正題,尼瑪我都快忘了要講的是metaclass啊。
還記的上面能夠暫時忽略的那段話嗎?type(X)是試圖實例化type對象,可是由於只有一個參數,因此源碼中只是返回了X的類。而type的標準初始化參數應當有三個:class_name, bases, attributes。最前面那個"class A(object): pass",python解釋器實際的流程是:
解析這段代碼,得知它須要建立一個【類對象】,這個類的名字叫作'A', 它的父類列表(用tuple表示)是 (object,),它的屬性用一個dict來表示就是 {} 。
查找用於生成這個類的metaclass。(終於講到重點了有木有!)
查找過程比較蛋疼,位於Python/ceval.c : build_class函數,按順序優先採用如下幾個:
2.1 定義中使用 __metaclass__ 屬性指定的(本例:沒有) 2.2 若是有父類,使用第一個父類的 __class__ 屬性,也就是父類的metaclass(本例:object的class,也就是type) 2.2.1 若是第一個父類沒有 __class__ 屬性,那就用父類的type(這是針對父類沒有父類的狀況) 2.3 使用當前Globals()中的 __metaclass__ 指定的(本例:沒有,不過2.2裏已經找到了) 2.4 使用PyClass_Type
注:2.2.1和2.4中提到了沒有父類,或者父類沒有父類的情形,這就是python中的old-style class,在python2.2以前全部的對象都是這樣的,而2.2以後能夠繼承於object類,就變成了new-style class。這種設計保持了向後兼容。
使用metaclass來建立這個A類。因爲A類的class就是metaclass,因此這個過程其實就是實例化metaclass的過程。本例中找到的metaclass是type,因此最終python執行的至關於這一句:
type('A', (object,), {})
再回想一下前面提到的實例化過程,實際上這一句分紅兩步: 1. 調用type.__new__(type, 'A', (object,), {})
生成type的一個實例(也就是A類對象);2. 調用type.__init__(A, 'A', (object,), {})
對A類對象進行初始化。注意:這裏調用的是type.__init__
,而不是A.__init__
:由於A是type的一個實例。
流程終於解釋完啦,不過我以爲仍是舉個栗子會比較好。就用我看到的那個有點二二的栗子吧:定義一個class,把它的全部屬性都改爲全大寫的。我感受這個栗子惟一的做用就是用來當栗子了。還好還有這個做用,不然連出生的機會都沒有。
直接上代碼好了:
def upper_meta(name, bases, attrs): new_attrs = {} for name, value in attrs.items(): if not name.startswith('__'): new_attrs[name.upper()] = value else: new_attrs[name] = value return type(name, bases, new_attrs) class Foo(object): __metaclass__ = upper_meta hello = 'world' print Foo.__dict__
請不要說「說好的metaclass呢!怎麼變成了一個函數!我摔!」
,回顧一下最最前面提到的一點:everything is an object in python。upper_meta做爲一個函數,它也是一個對象啊。而metaclass也不過就是個對象,並無本質上的差異——只要它被call的時候能接受name, bases, attrs這三個參數並返回一個類對象就好了。duck-typing的語言用起來就是有這樣的一種不可言狀的酸爽感。
理解了這一點,這段代碼就能理解了,upper_meta返回了一個type類的實例——也就是Foo類,而且能夠看到print出來的屬性裏頭只有HELLO而沒有hello。
考慮到可能有人不滿意,想看使用class來做爲metaclass的情形,我就勉爲其難換個姿式再舉一下這個栗子(真累)。
class upper_meta(type): def __new__(cls, name, bases, attrs): attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()]) return type(name, bases, attrs)
寫的太長了,換了一個短一點的oneliner,可是效果不變(其實我就是想炫一下,不服來咬我呀)。
這段代碼雖然形式上跟前面的upper_meta函數不同,可是本質是同樣的:調用了upper_meta('Foo', (object,), {'hello': 'world'})
,生成了一個新的名爲Foo的類對象。
理論上,故事講到這裏應該結束了,然而我想說,壓軸戲還沒上呢。
我要把這栗子舉得更高更遠,也更符合實際開發的需求:繼承。
class Bar(Foo): hi = 'there' print Bar.__dict__
這段代碼太簡單了,可是埋在下面的邏輯卻太複雜了。
它的輸出並非{'HI': 'there'}
, 而是{'hi': 'there'}
。你print Bar.HELLO, Bar.__metaclass__
都能獲得預期的輸出,可是恰恰沒有HI,只有hi。
爲何?這真是個燒腦細胞的事情。我已經把全部的邏輯都展示出來了,甚至還作了特別的標記。然而即使如此,想要把這個邏輯理順,也是一件很是有挑戰性的事情,幸虧我已經想明白了:苦海無涯,懸崖勒馬。啊呸,應該是——學海無涯苦做舟,不想明白不回頭。
我想說「甚至還作了特別標記」這句話的意思是,我還給【甚至】
這兩個字作了特別標記:在__new__方法中生成並返回的對象並無強制要求必定是該class的實例!
問題的關鍵就在這裏:前面兩個栗子中給出的upper_meta,返回的並非upper_meta的實例,而是type的實例,而是type的實例,而是type的實例
。重說三。
什麼意思?再看看代碼,最後return的是type(name, bases, attrs),也就是說,Foo類對象並非upper_meta的實例,而是type的實例(也就是說:雖然指定並被使用的metaclass是upper_meta,可是最終建立出來的Foo類的metaclass是type)。不信你print type(Foo)試試,結果就是type,而不是upper_meta。
爲何這會致使繼承於Foo類的Bar類不能由upper_meta來搭建?Bar.__metaclass__不仍是upper_meta嗎?
這個問題就沒有那麼困難了,有興趣的同窗能夠本身試着分析一下,沒興趣的大概也不會有耐心看到這裏吧。
Bar.__metaclass__並非Bar的原生屬性,而是繼承於Foo的——因此在print Bar.__dict__
的時候看不到__metaclass__。也就是說,在試圖建立Bar時,attrs裏並無__metaclass__屬性,因此並不會直接採用upper_meta。再回顧一下選擇metaclass的順序就能夠發現,實際上在2.2裏會選擇Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。
解決方法很簡單:關鍵就是前面被特別標記了的【應當】返回這個class的父類的__new__方法返回的對象。具體到代碼應當是這樣:
class upper_meta(type): def __new__(cls, name, bases, attrs): attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()]) return super(upper_meta, cls).__new__(cls, name, bases, attrs) def __init__(cls, name, bases, attrs): print >>sys.stderr, 'in upper_meta.__init__' #FOR TEST ONLY
新增的__init__方法並非必須的,有興趣的同窗能夠跟上面的栗子對比一下,因爲前面返回的是type類的實例,調用到的是type.__init__
;而這樣正確的寫法就會調用到upper_meta.__init__
。(p.s. super也是燒腦細胞的東西,但用於解決鑽石繼承的問頗有意思,有興趣的同窗能夠看看Cooperative methods and "super")
果真很燒腦細胞吧。
關於metaclass的選擇,還有另一個坑:在metaclass 2.3提到了,找不到metaclass的狀況下,會使用Globals()中定義的__metaclass__屬性指定的元類來建立類,那麼爲何下面的代碼卻沒有生效呢?
def __metaclass__(name, bases, attrs): attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()]) return type(name, bases, attrs) class Foo(object): hello = 'world' print Foo.__dict__
回到我最初的需求:我須要建立帶class initializer的類。爲何會有這樣的需求?最多見的metaclass的應用場景是對數據庫的封裝。舉例來講,我但願建立一個Table類,全部表都是繼承於這個類,同時我還想給每個表都設置一個緩存dict(使用主鍵做爲key緩存查詢結果)。一個很天然的想法是這樣的:
class Table(object): _pk_cache = {} @classmethod def cache(cls, obj): cls._pk_cache[obj.pkey()] = obj; @classmethod def findByPk(cls, pkey): return cls._pk_cache[pkey] def __init__(self, pkey, args): self._pkey = pkey self._args = args type(self).cache(self) def pkey(self): return self._pkey def __repr__(self): return type(self).__name__ + ':' + repr(self._args) class Student(Table): pass class Grade(Table): pass s1 = Student(1, 's1') g1 = Grade(1, 'g1') print Student.findByPk(1)
惋惜這是錯的。從輸出結果就能看出來,返回的是一個Grade對象,而不是預期的Student對象。緣由很簡單:子類們並不直接擁有_pk_cache ,它們訪問的是Table的_pk_cache ,而該dict只被初始化了一次。
固然,我能夠在每個繼承於Table的class裏新增一句 _pk_cache = {}
,可是這樣的實現太醜了,並且一不注意就會漏掉致使出錯。
因此我須要一個class initializer,在class被建立的時候,給它新增一個_pk_cache 。
在搞清楚了metaclass以後,解決方法特別簡單:
class TableInitializer(type): def __new__(cls, name, bases, attrs): attrs['_pk_cache'] = {} return super(TableInitializer, cls).__new__(cls, name, bases, attrs) class Table(object): __metaclass__ = TableInitializer ... #如下不變
完。