Metaclasses are deeper magic that 99% of users should never worry about. 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). —— Python界的領袖 Tim Peters
Zen of Python的 作者Tim Peters大神說99%的Python用戶根本不需要為元類操心。雖然大神這么說了,但我認為還是有必要了解下這個Python的黑魔法,至少要知道它是什么東東。
關于元類的介紹網上已經有很多資料了,目前為止我認為介紹的最好的是《深刻理解Python中的元類(metaclass)》這篇從Stackoverflow翻譯過來的資料。
但是還有些瑕疵,比如__metaclass__如果定義在模塊頂層,那么只有對舊式類才會起作用,而它沒有對這點進行說明。所以如果可以話,請直接閱讀英文原文
讀完上面的文檔之后,我對于元類有了個大概的了解,但是其中的一些細節還是需要進一步實踐驗證,下面我就通過代碼來展示一些我對元類細節的理解。
注意:因為本人是使用Python 2.7為主,所以下文除了標明了Python 3.x的部分,其余都是基于Python 2.7的
元類的本質
元類就是用來創建類的“東西”,這句話必須要牢牢記住,元類是用來創建類的。我們知道類可以用來創建實例,而元類它的“實例”就是另一個類。
MyClass = MetaClass()
MyObject = MyClass()
__metaclass__
Python 2.7通過__metaclass__屬性來實現元類功能
def create_class(name, bases, attr): #注意必須有三個參數
return type(name, bases, attr)
class MyClass(object):
__metaclass__ = create_class
class MetaClass(type):
pass
__metaclass__ = MetaClass
class MyClass1:
pass
class MyClass2(object):
__metaclass__ = MetaClass
在Python 2.7版本里__metaclass__屬性既可以是一個函數,也可以是一個類,只要這個東西在執行之后能返回一個類就行。
另外注意在模塊定義的__metaclass__屬性只對舊式類起作用
解釋器在生成類對象的時候會首選在當前類定義中查找是否有__metaclass__屬性,如果有則按照當前類定義的__metaclass__屬性生成類,否則繼續去父類中查找__metaclass__屬性,如果所有父類中都沒有找到__metaclass__屬性,則繼續在模塊中查找,如果還是沒有找到,則按照類定義生成類對象。(對于新式類,沒有查找模塊__metaclass__屬性這步)
所以__metaclass__屬性對解釋器而言就是一個生成類對象的Hook,它會攔截解釋器正常生成類對象的流程。
Python 3.x取消了__metaclass__屬性,改為
class MyClass3(metaclass=MetaClass): # MetaClass也可以是個函數
pass
這種方式來實現元類。
元類構造過程
class MetaClass(type):
def __init__(cls, name, bases, attr):
print "MetaClass __init__:", cls.__name__
def __new__(cls, name, bases, attr):
print "MetaClass __new__:", cls.__name__
new_cls = super(MetaClass, cls).__new__(cls, name, bases, attr)
print 'MetaClass create class:', new_cls.__name__
return new_cls
def __call__(cls, *args, **kwargs):
print "MetaClass __call__:", cls.__name__
return super(MetaClass, cls).__call__(cls, *args, **kwargs)
元類的定義和普通類相同,唯一的區別就是元類必須繼承type或者其他元類。
實例化過程也和普通類一樣,元類在實例化之前會調用__new__方法來生成一個新的類,這就是元類和普通類的最大區別,接著__new__方法會將新生成的類傳遞給__init__方法。和普通類一樣,如果__new__方法不返回一個類,__init__方法不會被調用。
class MyClass(object):
__metaclass__ = MetaClass
def __init__(self, *args, **kwargs):
print "MyClass __init__"
def __new__(cls, *args, **kwargs):
print "MyClass __new__"
return super(MyClass, cls).__new__(cls, *args, **kwargs)
def __call__(self, *args, **kwargs):
print "MyClass __call__"
代碼執行之后輸出
MetaClass __new__: MetaClass
MetaClass create class: MyClass
MetaClass __init__: MyClass
Python中一切都是對象,包括類,所以解釋器會在解析到類定義的時候,生成一個當前命名空間中唯一的一個類對象。而我在上面說過__metaclass__屬性會攔截生解釋器生成類對象的過程,上面的輸出就證明了這點。
__call__方法在生成類的過程中不會被調用,它會在元類生成的類生成實例之前被調用,有點繞,直接看代碼比較清晰。
# MyClass類定義之后加上下面的語句用來生成一個類實例
mc1 = MyClass()
代碼執行之后輸出
MetaClass __new__: MetaClass
MetaClass create class: MyClass
MetaClass __init__: MyClass
MetaClass __call__: MyClass
MyClass __new__
MyClass __init__
可以看到元類的__call__方法被調用了。
為什么呢?
首選來回顧下__call__方法會在什么情況下被調用?
類如果定義了__call__方法那么就表明類的實例也是一個可調用的對象,比如
class Person(object):
def __init__(self, name):
self.name = name
def __call__(self):
print "Hello", self.name
>>> p = Person('Tim')
>>> p()
Hello Tim
如果沒有__call__方法,類實例p無法被調用。
回到主題,MyClass可以被視作是元類MetaClass的實例,雖然它是一個類,又因為元類實現了__call__方法,所以元類的實例是可調用的。所以在執行MyClass()的時候,才會調用元類的__call__方法。
再說一句,元類繼承type之后,可以不用自己實現__call__方法,它的實例就是可調用的,這也是元類和普通類的一個區別。
元類__new__方法的參數
Python 2.7版本中元類的__new__方法會傳遞4個參數
def __new__(cls, name, bases, attr):
print "MetaClass.__new__(cls=%s, name=%r, bases=%s, attrs=[%s])" % (cls, name, bases, ", ".join(attr))
return super(MetaClass, cls).__new__(cls, name, bases, attr)
輸出
MetaClass.__new__(cls=<class '__main__.MetaClass'>, name='MyClass', bases=(<type 'object'>,), attrs=[__call__, __module__, __metaclass__, __new__, __init__])
MetaClass __init__: MyClass
cls:元類對象
name:要生成的類的類名
bases:要生成的類的所有父類,是個元組(tuple)
attr:要生成類的所有屬性,是個字典
在Python 3.x版本上__new__方法的參數有了變動,可以傳遞額外的參數了。
class MetaClass(type):
def __new__(cls, name, bases, attr, **options):
print('name=%s, bases=%s, attr=[%s], **%s' % (name, bases, ', '.join(attr), options))
return super(MetaClass, cls).__new__(cls, name, bases, attr)
class MyClass(metaclass=MetaClass, extra=1):
pass
輸出
name=MyClass, bases=(), attr=[__module__, __qualname__], **{'extra': 1}
可以看到extra作為一個關鍵字參數被傳遞給了__new__方法
總結
元類作為Python中的黑魔法,雖然用到的機會不多,但技多不壓身,多懂點總是沒有錯的。何況Django的ORM就用到了元類,了解元類可以更好的理解ORM代碼。推薦廖雪峰老師用元類實現ORM的教程——Day 3 - 編寫ORM,看了之后會對元類有更深的理解。
以上就是目前我對元類的全部理解,記于 2018-03-27
參考:
http://blog.jobbole.com/21351/
https://stackoverflow.com/a/6581949/9500863
https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
http://martyalchin.com/2011/jan/20/class-level-keyword-arguments/