理解Python中的元類(metaclass)
這篇文章基本上是What are metaclasses in Python?最高贊答案的翻譯,同時我簡化了一點東西,加了點自己的demo。可以直接去看原文。
類也是對象
在理解元類之前,你需要先掌握Python中的類。Python中類的概念借鑑於Smalltalk,這顯得有些奇特。在大多數編程語言中,類就是一組用來描述如何生成一個對象的代碼段。在Python中這一點仍然成立:
>>> class ObjectCreator(object):
... pass
...
>>> my_object = ObjectCreator()
>>> print(my_object)
<__main__.ObjectCreator object at 0x8974f2c>
但是,Python中的類還遠不止如此。類同樣也是一種對象。是的,沒錯,就是對象。只要你使用關鍵字class,Python解釋器在執行的時候就會創建一個對象.
下面這段代碼:
class ObjectCreator(object):
pass
將在內存中創建一個對象,名字就是ObjectCreator。這個對象(類)自身擁有創建對象(類實例)的能力,而這就是爲什麼它是一個類的原因。但是,它的本質仍然是一個對象,於是你可以對它做如下的操作:
- 你可以將它賦值給一個變量
- 你可以拷貝它
- 你可以爲它增加屬性
- 你可以將它作爲函數參數進行傳遞
下面是示例:
>>> print ObjectCreator # 你可以打印一個類,因爲它其實也是一個對象
<class '__main__.ObjectCreator'>
>>> def echo(o):
… print o
…
>>> echo(ObjectCreator) # 你可以將類做爲參數傳給函數
<class '__main__.ObjectCreator'>
>>> print hasattr(ObjectCreator, 'new_attribute')
Fasle
>>> ObjectCreator.new_attribute = 'foo' # 你可以爲類增加屬性
>>> print hasattr(ObjectCreator, 'new_attribute')
True
>>> print ObjectCreator.new_attribute
foo
>>> ObjectCreatorMirror = ObjectCreator # 你可以將類賦值給一個變量
>>> print ObjectCreatorMirror()
<__main__.ObjectCreator object at 0x8997b4c>
動態地創建類
因爲類也是對象,你可以在運行時動態的創建它們,就像其他任何對象一樣。首先,你可以在函數中創建類,使用class關鍵字即可
>>> def choose_class(name):
… if name == 'foo':
… class Foo(object):
… pass
… return Foo # 返回的是類,不是類的實例
… else:
… class Bar(object):
… pass
… return Bar
…
>>> MyClass = choose_class('foo')
>>> print MyClass # 函數返回的是類,不是類的實例
<class '__main__'.Foo>
>>> print MyClass() # 你可以通過這個類創建類實例,也就是對象
<__main__.Foo object at 0x89c6d4c>
但這還不夠動態,因爲你仍然需要自己編寫整個類的代碼。由於類也是對象,所以它們必須是通過什麼東西來生成的纔對。
type能動態的創建類,type可以接受一個類的描述作爲參數,然後返回一個類。
type(類名, 父類的元組(針對繼承的情況,可以爲空),包含屬性的字典(名稱和值))
比如下面的代碼:
>>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一個類對象
>>> print MyShinyClass
<class '__main__.MyShinyClass'>
>>> print MyShinyClass() # 創建一個該類的實例
<__main__.MyShinyClass object at 0x8997cec>
type 接受一個字典來爲類定義屬性,因此:
>>> class Foo(object):
… bar = True
等同於:
Foo = type('Foo', (), {'bar':True})
在Python中,類也是對象,你可以動態的創建類。這就是當你使用關鍵字class時Python在幕後做的事情,而這就是通過元類來實現的。
元類
元類就是創建類這種對象的東西。如果你喜歡的話,可以把元類稱爲“類工廠”。type就是Python的內建元類,當然了,你也可以創建自己的元類。
元類本身而言,它們其實是很簡單的:
攔截類的創建
修改類
返回修改之後的類
自定義元類
元類的主要目的就是爲了當創建類時能夠自動地改變類。
Python 3中創建元類的語法:
class NothingMetaclass(type):
def __new__(mcs, name, bases, namespace):
# 什麼都沒做,你可以在這裏做點什麼
return type.__new__(mcs, name, bases, namespace)
class Foo(object, metaclass=NothingMetaclass):
pass
demo1
- new是一個靜態方法,而init是一個實例方法.
- new方法會返回一個創建的實例,而init什麼都不返回.
- 只有在new返回一個cls的實例時後面的init才能被調用.
- 當創建一個新實例時調用new,初始化一個實例時用init.
看一個例子
# 請記住,'type'實際上是一個類,就像'str'和'int'一樣
# 所以,你可以從type繼承
class MetaA(type):
# __new__ 是在__init__之前被調用的特殊方法
# __new__是用來創建對象並返回之的方法
# 而__init__只是用來將傳入的參數初始化給對象
# 你很少用到__new__,除非你希望能夠控制對象的創建
# 這裏,創建的對象是類,我們希望能夠自定義它,所以我們這裏改寫__new__
# 如果你希望的話,你也可以在__init__中做些事情
# 還有一些高級的用法會涉及到改寫__call__特殊方法,但是我們這裏不用
def __new__(cls, name, bases, dct):
print('MetaA.__new__')
# 這種方式不會調用__init__方法
# return type(name, bases, dct)
# 這種方式會調用__init__
return type.__new__(cls, name, bases, dct)
def __init__(cls, name, bases, dct):
print('MetaA.__init__')
class A(object, metaclass=MetaA):
pass
print(A())
demo2
class ListMetaclass(type):
# 元類會自動將你通常傳給‘type’的參數作爲自己的參數傳入
# mcs表示元類
# name表示創建類的類名(在這裏創建類就是繼承Model類的子類User)
# bases表示創建類繼承的所有父類
# namespace表示創建類的所有屬性和方法(以鍵值對的字典的形式)
def __new__(mcs, name, bases, namespace):
namespace['add'] = lambda self, value: self.append(value)
return type.__new__(mcs, name, bases, namespace)
# 通過metaclass,給該類動態添加了add方法
class MyList(list, metaclass=ListMetaclass):
pass
l = MyList()
l.add(1)
print(l)
究竟爲什麼要使用元類?
現在回到我們的大主題上來,究竟是爲什麼你會去使用這樣一種容易出錯且晦澀的特性?好吧,一般來說,你根本就用不上它:
“元類就是深度的魔法,99%的用戶應該根本不必爲此操心。如果你想搞清楚究竟是否需要用到元類,那麼你就不需要它。那些實際用到元類的人都非常清楚地知道他們需要做什麼,而且根本不需要解釋爲什麼要用元類。” —— Python界的領袖 Tim Peters
元類的主要用途是創建API。一個典型的例子是Django ORM。它允許你像這樣定義
class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
然後可以通過簡單點API操作數據庫。其背後的魔法就是定義了元類,並且使用了一些魔法能夠將你剛剛定義的簡單的Person類轉變成對數據庫的一個複雜hook。Django框架將這些看起來很複雜的東西通過暴露出一個簡單的使用元類的API將其化簡,通過這個API重新創建代碼,在背後完成真正的工作。
一個簡單的orm demo
class Field:
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)
class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
class ModelMetaclass(type):
def __new__(mcs, name, bases, attrs):
if name == 'Model':
return type.__new__(mcs, name, bases, attrs)
print("Found Model: %s" % name)
mapping = dict()
fields = list()
# 將屬性保存到mapping中
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping : %s ==> %s' % (k, v))
mapping[k] = v
fields.append(k)
# 將Model中的Field刪除
for k in mapping.keys():
attrs.pop(k)
attrs['__fields__'] = list(map(lambda f: '`%s`' % f, fields))
attrs['__mapping__'] = mapping
attrs['__table__'] = name
return type.__new__(mcs, name, bases, attrs)
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kwargs):
super(Model, self).__init__(kwargs)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k, v in self.__mapping__.items():
print("%s------%s" % (k, v))
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(self.__fields__), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
class User(Model):
# 定義類的屬性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
u = User(id=12345, name='Michael', email='[email protected]', password='my-pwd')
u.save()
結語
Python中的一切都是對象,它們要麼是類的實例,要麼是元類的實例,除了type。type實際上是它自己的元類,在純Python環境中這可不是你能夠做到的,這是通過在實現層面耍一些小手段做到的。其次,元類是很複雜的。對於非常簡單的類,你可能不希望通過使用元類來對類做修改。你可以通過其他兩種技術來修改類:
1)Monkey patching
- 類裝飾器
當你需要動態修改類時,99%的時間裏你最好使用上面這兩種技術。當然了,其實在99%的時間裏你根本就不需要動態修改類