摘要:最前面那個,解釋器實際的流程是解析這段代碼,得知它需要創(chuàng)建一個類對象,這個類的名字叫做它的父類列表用表示是,它的屬性用一個來表示就是。解決方法很簡單關鍵就是前面被特別標記了的應當返回這個的父類的方法返回的對象。
(原發(fā)于我的blog:Python: metaclass小記 )
友情提示:本文不一定適合閱讀,如果執(zhí)意要讀,請備好暈車藥。
題記起因"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don"t."
-- Tim Peters
這句話聽起來就很誘人,曾經(jīng)試圖去理解它,但是因為沒有實際的需求,就因為燒腦子而放棄了。不妨摘錄一段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).
昨天心血來潮想寫一個帶class initializer的class,發(fā)現(xiàn)繞不過metaclass了,于是又翻出來看。
概述其實是要理解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,
)
print (A, id(A), type(A))
(
, 11991040, )
print (type, id(type), type(type))
(
, 1891232, )
其中第一個print很好理解:a是一個A的實例,有自己的id(其實就是內(nèi)存地址)、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類的一個對象。
唉,本來想好好解釋的,沒想到還是說成繞口令了。算了,反正我懂了,繼續(xù)。
type沒有仔細了解type是什么的同學可能會以為type是一個函數(shù):type(X)用于返回X的類對象。
然而并不完全是這樣的:在python里,X(args)可能是調用一個函數(shù),也可能是在實例化一個X的對象——而很不幸地,type(X)實際上是介于二者之間的一個調用:雖然type是一個class,但是它的__call__方法是存在的,于是python把它當成一個函數(shù)來調用,實際調用到了源碼中的type_call;type_call調用了type.__new__試圖初始化一個type類的實例,然而type.__new__(位于源碼中的type_new函數(shù))發(fā)現(xiàn)臥槽居然只有一個參數(shù),于是就返回了這個參數(shù)的type(源碼是這么寫的:"return (PyObject *) Py_TYPE(x);",并沒有生成新的對象)。也就是說,本來是個函數(shù)調用,里面卻是要初始化一個對象,然而最后返回的卻不是初始化的對象!尼瑪那個特殊情況為毛不放到函數(shù)調用里面啊,開發(fā)者腦抽了嗎!
感到腦抽的同學可以暫時忽略上面那段話,跟本文沒太大關系。繼續(xù)。
實際上type是在builtin模塊中定義,指向源碼中PyType_Type對象的一個引用:
//位于Python/bltinmodule.c PyObject * _PyBuiltin_Init(void) { ... SETBUILTIN("type", &PyType_Type); ... }
這個PyType_Type又是個什么鬼?好吧,繼續(xù)貼源碼
//位于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環(huán)境中的class為X),所以前面第三個print中可以看到,type(type) == type(哈哈哈,寫繞口令真好玩)
在PyType_Type的定義指定了 tp_init = type_init 和 tp_new = type_new 這兩個屬性值。這是兩個函數(shù),也位于源碼中的Object/typeobject.c。
關于第3點,在Python document中關于__new__方法的說明里有詳細的介紹,這里簡單總結一下:在new一個對象的時候,只會調用這個class的__new__方法,它需要生成一個object、調用這個對象的__init__方法對它進行初始化,然后返回這個對象。
好吧,我發(fā)現(xiàn)不得不把簡單總結展開,否則確實說不清楚。
實例化這是一個很有意思的設計:把實例化的流程暴露給碼農(nóng),意味著碼農(nóng)可以在對象的生成前、生成后返回前兩個環(huán)節(jié)對這個對象進行修改(【甚至】在__new__方法中生成并返回的對象并沒有強制要求一定是該class的實例!不過在document里建議,如果要覆蓋__new__方法,那么【應當】返回這個class的父類的__new__方法返回的對象)。這里還有一個非常tricky的地方:雖然沒有明確指定,但是__new__方法被硬編碼為一個staticmethod(有興趣的話可以去翻type_new函數(shù)),它的第一個參數(shù)是需要被實例化的class,其余參數(shù)則是需要傳給__init__的參數(shù)。
說起來非常枯燥,還是舉一個例子吧,就用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參數(shù),前面說了,它是需要被實例化的class,也就是說,最后一行實際執(zhí)行的是:
DbConnection.__new__(DbConnection, db_config)
而DbConnection的__new__方法直接繼承于Singleton, 所以實際調用的是
Singleton.__new__(DbConnection, db_config)
主要注意的地方,在上面這段代碼的第六行,Singleton是繼承于object(這里特指python中的那個object對象),因此調用了object.__new__(DbConnection)來生成一個對象,生成過程位于C源碼中的object_new函數(shù)(Objects/typeobject.c),它會將新生成對象的type指定為DbConnection,然后直接返回。
Singleton.__new__在拿到了生成的DbConnection實例以后,將它保存在了DbConnection類的__it__屬性中,然后對該實例進行初始化,最后返回。
可以看到,任何繼承于Singleton類的子類,只要不覆蓋其__new__方法,每個類永遠只會被實例化一次。
好了,第2點暫告一段落,接下來回歸正題,尼瑪我都快忘了要講的是metaclass啊。
metaclass還記的上面可以暫時忽略的那段話嗎?type(X)是試圖實例化type對象,但是因為只有一個參數(shù),所以源碼中只是返回了X的類。而type的標準初始化參數(shù)應當有三個:class_name, bases, attributes。最前面那個"class A(object): pass",python解釋器實際的流程是:
解析這段代碼,得知它需要創(chuàng)建一個【類對象】,這個類的名字叫做"A", 它的父類列表(用tuple表示)是 (object,),它的屬性用一個dict來表示就是 {} 。
查找用于生成這個類的metaclass。(終于講到重點了有木有!)
查找過程比較蛋疼,位于Python/ceval.c : build_class函數(shù),按順序優(yōu)先采用以下幾個:
2.1 定義中使用 __metaclass__ 屬性指定的(本例:沒有) 2.2 如果有父類,使用第一個父類的 __class__ 屬性,也就是父類的metaclass(本例:object的class,也就是type) 2.2.1 如果第一個父類沒有 __class__ 屬性,那就用父類的type(這是針對父類沒有父類的情況) 2.3 使用當前Globals()中的 __metaclass__ 指定的(本例:沒有,不過2.2里已經(jīng)找到了) 2.4 使用PyClass_Type
注:2.2.1和2.4中提到了沒有父類,或者父類沒有父類的情形,這就是python中的old-style class,在python2.2之前所有的對象都是這樣的,而2.2之后可以繼承于object類,就變成了new-style class。這種設計保持了向后兼容。
使用metaclass來創(chuàng)建這個A類。由于A類的class就是metaclass,所以這個過程其實就是實例化metaclass的過程。本例中找到的metaclass是type,所以最終python執(zhí)行的相當于這一句:
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呢!怎么變成了一個函數(shù)!我摔!”,回顧一下最最前面提到的一點:everything is an object in python。upper_meta作為一個函數(shù),它也是一個對象啊。而metaclass也不過就是個對象,并沒有本質上的差別——只要它被call的時候能接受name, bases, attrs這三個參數(shù)并返回一個類對象就行了。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函數(shù)不一樣,但是本質是一樣的:調用了upper_meta("Foo", (object,), {"hello": "world"}),生成了一個新的名為Foo的類對象。
理論上,故事講到這里應該結束了,然而我想說,壓軸戲還沒上呢。
壓軸戲我要把這栗子舉得更高更遠,也更符合實際開發(fā)的需求:繼承。
class Bar(Foo): hi = "there" print Bar.__dict__
這段代碼太簡單了,但是埋在下面的邏輯卻太復雜了。
它的輸出并不是{"HI": "there"}, 而是{"hi": "there"}。你print Bar.HELLO, Bar.__metaclass__都能得到預期的輸出,但是偏偏沒有HI,只有hi。
為什么?這真是個燒腦細胞的事情。我已經(jīng)把所有的邏輯都展現(xiàn)出來了,甚至還做了特別的標記。然而即便如此,想要把這個邏輯理順,也是一件非常有挑戰(zhàn)性的事情,幸好我已經(jīng)想明白了:苦海無涯,回頭是岸。啊呸,應該是——學海無涯苦作舟,不想明白不回頭。
我想說“甚至還做了特別標記”這句話的意思是,我還給【甚至】這兩個字做了特別標記:在__new__方法中生成并返回的對象并沒有強制要求一定是該class的實例!
問題的關鍵就在這里:前面兩個栗子中給出的upper_meta,返回的并不是upper_meta的實例,而是type的實例,而是type的實例,而是type的實例。重說三。
什么意思?再看看代碼,最后return的是type(name, bases, attrs),也就是說,F(xiàn)oo類對象并不是upper_meta的實例,而是type的實例(也就是說:雖然指定并被使用的metaclass是upper_meta,但是最終創(chuàng)建出來的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__。也就是說,在試圖創(chuàng)建Bar時,attrs里并沒有__metaclass__屬性,所以并不會直接采用upper_meta。再回顧一下選擇metaclass的順序就可以發(fā)現(xiàn),實際上在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__屬性指定的元類來創(chuàng)建類,那么為什么下面的代碼卻沒有生效呢?
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
回到我最初的需求:我需要創(chuàng)建帶class initializer的類。為什么會有這樣的需求?最常見的metaclass的應用場景是對數(shù)據(jù)庫的封裝。舉例來說,我希望創(chuàng)建一個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 = {},但是這樣的實現(xiàn)太丑了,而且一不注意就會漏掉導致出錯。
所以我需要一個class initializer,在class被創(chuàng)建的時候,給它新增一個_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 ... #以下不變
完。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/37669.html
摘要:但我并不是一個翻譯者并不會嚴格遵守每行每句的翻譯有時候我會將表述換個順序省略一些我認為無關緊要的話,以便讀者更好理解。類也是對象在理解之前,我們先要掌握中的類是什么。這個對象類自身擁有產(chǎn)生對象實例的能力。不過這樣做是為了保持向后兼容性。 前言 這篇博客是我在stackoverflow上看了一個提問回復后寫的,例子基本用的都是e-satis本人的例子,語言組織也基本按照翻譯來。 但我并不...
摘要:先簡單介紹下中的元類。元類就是創(chuàng)建類的類,對于元類來說,類是它的實例,將返回。中的所有類,都是的實例,換句話說,是元類的基類。 我在看源代碼的時候,經(jīng)常蹦出這一句:How does it work!竟然有這種操作?本系列文章,試圖剖析代碼中發(fā)生的魔法。順便作為自己的閱讀筆記,以作提高。 先簡單介紹下Python中的元類(metaclass)。元類就是創(chuàng)建類的類,對于元類來說,類是它的實...
摘要:原鏈接中的元類是什么類也是對象在理解元類之前,需要掌握中類概念。事實上,是中用于創(chuàng)建所有類的元類。類本身是元類的對象在中,除了,一切皆對象,一切都是類或者元類的對象。事實上是自己的元類, 學習契機 項目中使用Elasticsearch(ES)存儲海量業(yè)務數(shù)據(jù),基于ES向外提供的API進一層封裝,按需處理原始數(shù)據(jù)提供更精確、更多樣化的結果。在研究這一層的代碼時接觸到@six.add_me...
摘要:什么是元類剛才說了,元類就是創(chuàng)建類的類。類上面的屬性,相信愿意了解元類細節(jié)的盆友,都肯定見過這個東西,而且為之好奇。使用了這個魔法方法就意味著就會用指定的元類來創(chuàng)建類了。深刻理解中的元類 (一) python中的類 今天看到一篇好文,然后結合自己的情況總結一波。這里討論的python類,都基于python2.7x以及繼承于object的新式類進行討論。 首先在python中,所有東西都...
摘要:但一般情況下,我們使用類作為元類。那么,元類到底有什么用呢要你何用元類的主要目的是為了控制類的創(chuàng)建行為。當然,有很多種做法,這里展示用元類的做法。當你創(chuàng)建類時,解釋器會調用元類來生成它,定義一個繼承自的普通類意味著調用來創(chuàng)建它。 元類 Python 中的元類(metaclass)是一個深度魔法,平時我們可能比較少接觸到元類,本文將通過一些簡單的例子來理解這個魔法。 類也是對象 在 Py...
閱讀 2616·2021-09-28 09:35
閱讀 3265·2021-09-03 10:28
閱讀 2913·2019-08-30 15:43
閱讀 1480·2019-08-30 14:04
閱讀 1808·2019-08-29 17:02
閱讀 1818·2019-08-26 13:59
閱讀 697·2019-08-26 11:51
閱讀 3261·2019-08-23 17:16