什么是元類?
理解元類(metaclass)之前,我們先了解下Python中的OOP和類(Class)
面向對象全稱 Object Oriented Programming 簡稱OOP别洪,這種編程思想被大家所熟知。它是把對象作為一個程序的基本單元勃蜘,把數(shù)據(jù)和功能封裝在里面簿训,能夠實現(xiàn)很好的復用性,靈活性和擴展性害幅。OOP中有2個基本概念:類和對象:
- 類是描述如何創(chuàng)建一個對象的代碼段消恍,用來描述具有相同的屬性和方法的對象的集合,它定義了該集合中每個對象所共有的屬性和方法
- 對象是類的實例(Instance)以现。
我們舉個例子:
In : class ObjectCreator(object):
...: pass
...:
In : my_object = ObjectCreator()
In : my_object
Out: <__main__.ObjectCreator at 0x1082bbef0> # 表示它是ObjectCreator 的實例的意思
而Python中的類并不是僅限于此:
In : print(ObjectCreator)
<class '__main__.ObjectCreator'>
ObjectCreator竟然可以被print
狠怨,所以它的類也是對象!既然類是對象邑遏,你就能動態(tài)地創(chuàng)建它們佣赖,就像創(chuàng)建任何對象那樣。我在日常工作里面就會有這種動態(tài)創(chuàng)建類的需求记盒,比如在mock數(shù)據(jù)的時候憎蛤,現(xiàn)在有個函數(shù)func接收一個參數(shù)
In : def func(instance):
...: print(instance.a, instance.b)
...: print(instance.method_a(10))
...:
正常使用起來傳入的instance是符合需求的(有a、b屬性和method_a方法)纪吮,但是當我想單獨調試func的時候俩檬,需要「造」一個,假如不用元類碾盟,應該是這樣寫:
In : def generate_cls(a, b):
...: class Fake(object):
...: def method_a(self, n): # Fake類的方法
...: return n
...: Fake.a = a
...: Fake.b = b
...: return Fake # return 后棚辽,generate_cls函數(shù)就會變成Fake類的實例,自然就擁有了method_a方法
...:
In : ins = generate_cls(1, 2)()
In : ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)
你會發(fā)現(xiàn)這不算是「動態(tài)創(chuàng)建」的:
- 類名(Fake)不方便改變
- 要創(chuàng)建的類需要的屬性和方法越多冰肴,就要對應的加碼屈藐,不靈活榔组。
怎么做呢:
In : def method_a(self, n):
...: return n
...: # 創(chuàng)建Fake類
In : ins = type('Fake', (), {'a': 1, 'b': 2, 'method_a': method_a})()
In : ins.a, ins.b, ins.method_a(10)
Out: (1, 2, 10)
到了這里,引出了type函數(shù)联逻。本來它用來能讓你了解一個對象的類型:
In : type(1)
Out: int
In : type('1')
Out: str
In : type(ObjectCreator)
Out: type
In : type(ObjectCreator())
Out: __main__.ObjectCreator
另外搓扯,type如上所說還可以動態(tài)地創(chuàng)建類:type可以把對于類的描述作為參數(shù),并返回一個類包归。
用來創(chuàng)建類的東東就是「元類」锨推,放張圖吧:
MyClass = type('MyClass', (), {})
這種用法就是由于type實際上是一個元類,作為元類的type在Python中被用于在后臺創(chuàng)建所有的類箫踩。在Python語言上有個說法「Everything is an object」爱态。包括整數(shù)、字符串境钟、函數(shù)和類... 所有這些都是對象锦担。所有這些都是由一個類創(chuàng)建的:
In : age = 35
In : age.__class__
Out: int
In : name = 'bob'
In : name.__class__
Out: str
現(xiàn)在,任何__class__
中的__class__
是什么慨削?
In : age.__class__.__class__
Out: type
In : name.__class__.__class__
Out: type
...
如果你愿意洞渔,你可以把type
稱為「類工廠」。type是Python中內建元類缚态,當然磁椒,你也可以創(chuàng)建你自己的元類。
創(chuàng)建自己的元類
Python2創(chuàng)建類的時候玫芦,可以添加一個__metaclass__
屬性:
class Foo(object):
__metaclass__ = something...
[...]
如果你這樣做浆熔,Python會使用元類來創(chuàng)建Foo這個類。Python會在類定義中尋找__metaclass__
桥帆。如果找到它医增,Python會用它來創(chuàng)建對象類Foo。如果沒有找到它老虫,Python將使用type
來創(chuàng)建這個類叶骨。
在Python3中語法改變了一下:
class Simple1(object, metaclass=something...):
[...]
本質上是一樣的。
def __new__(cls, name, bases, attrs):
def __init__(cls, func):
cls.func = func
def hello(cls):
print 'hello world'
t = type.__new__(cls, name, bases, attrs)
t.__init__ = __init__
t.hello = hello
return t
class New_Hello(object):
__metaclass__ = HelloMeta
New_Hello
初始化需要添加一個參數(shù)祈匙,并包含一個叫做hello的方法:
In : h = New_Hello(lambda x: x)
In : h.func(10), h.hello()
hello world
Out: (10, None)
PS: 這個例子只能運行于Python2忽刽。
在Python里__new__
方法創(chuàng)建實例,init負責初始化一個實例夺欲。對于type也是一樣的效果跪帝,只不過針對的是「類」,在上面的HelloMeta中只使用了
new創(chuàng)建類些阅,我們再感受一個使用
init`的元類:
In : class HelloMeta2(type):
...: def __init__(cls, name, bases, attrs):
...: super(HelloMeta2, cls).__init__(name, bases, attrs)
...: attrs_ = {}
...: for k, v in attrs.items():
...: if not k.startswith('__'):
...: attrs_[k] = v
...: setattr(cls, '_new_dict', attrs_)
...:
別往下看歉甚。思考下這樣創(chuàng)建出來的類有什么特殊的地方?
我揭曉一下(這次使用Python 3語法):
In : class New_Hello2(metaclass=HelloMeta2):
...: a = 1
...: b = True
In : New_Hello2._new_dict
Out: {'a': 1, 'b': True}
In : h2 = New_Hello2()
In : h2._new_dict
Out: {'a': 1, 'b': True}
有點明白么扑眉?其實就是在創(chuàng)建類的時候把類的屬性循環(huán)了一遍把不是__開頭的屬性最后存在了_new_dict
上纸泄。
什么時候需要用元類?
日常的業(yè)務邏輯開發(fā)是不太需要使用到元類的腰素,因為元類是用來攔截和修改類的創(chuàng)建的聘裁,用到的場景很少。我能想到最典型的場景就是 ORM弓千。ORM就是「對象 關系 映射」的意思衡便,簡單的理解就是把關系數(shù)據(jù)庫的一張表映射成一個類,一行記錄映射為一個對象洋访。ORM框架中的Model只能動態(tài)定義镣陕,因為這個模式下這些關系只能是由使用者來定義,元類再配合描述符就可以實現(xiàn)ORM了(參董大)姻政。
廖雪峰元類講解 二種方法-type/metaclass
type()
type()
函數(shù)可以查看一個類型或變量的類型呆抑,Hello是一個class,它的類型就是type汁展,而h是一個實例鹊碍,它的類型就是class Hello。
我們說class的定義是運行時動態(tài)創(chuàng)建的食绿,而創(chuàng)建class的方法就是使用type()函數(shù)侈咕。
type()
函數(shù)既可以返回一個對象的類型,又可以創(chuàng)建出新的類型器紧,比如耀销,我們可以通過type()函數(shù)創(chuàng)建出Hello類,而無需通過class Hello(object)...
的定義:
>>> def fn(self, name='world'): # 先定義函數(shù)
... print('Hello, %s.' % name) # 輸入 fn(1) 結果也是Hello,world
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 創(chuàng)建Hello class铲汪,type的套路
>>> h = Hello() # h是一個實例熊尉,它有一個hello的函數(shù)
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>
要創(chuàng)建一個class對象,type()函數(shù)依次傳入3個參數(shù):
- class的名稱桥状;
- 繼承的父類集合帽揪,注意Python支持多重繼承,如果只有一個父類辅斟,別忘了tuple的單元素寫法转晰;
- class的方法名稱與函數(shù)綁定,這里我們把函數(shù)fn綁定到方法名hello上士飒。
通過type()
函數(shù)創(chuàng)建的類和直接寫class是完全一樣的查邢,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法酵幕,然后調用type()
函數(shù)創(chuàng)建出class扰藕。
正常情況下,我們都用class Xxx...
來定義類芳撒,但是邓深,type()
函數(shù)也允許我們動態(tài)創(chuàng)建出類來未桥,也就是說,動態(tài)語言本身支持運行期動態(tài)創(chuàng)建類芥备,這和靜態(tài)語言有非常大的不同冬耿,要在靜態(tài)語言運行期創(chuàng)建類,必須構造源代碼字符串再調用編譯器萌壳,或者借助一些工具生成字節(jié)碼實現(xiàn)亦镶,本質上都是動態(tài)編譯,會非常復雜
metaclass
除了使用type()
動態(tài)創(chuàng)建類以外袱瓮,要控制類的創(chuàng)建行為缤骨,還可以使用metaclass。
metaclass尺借,直譯為元類绊起,簡單的解釋就是:
當我們定義了類以后,就可以根據(jù)這個類創(chuàng)建出實例褐望,所以:先定義類勒庄,然后創(chuàng)建實例。
但是如果我們想創(chuàng)建出類呢瘫里?那就必須根據(jù)metaclass創(chuàng)建出類实蔽,所以:先定義metaclass,然后創(chuàng)建類谨读。
連接起來就是:先定義metaclass局装,就可以創(chuàng)建類,最后創(chuàng)建實例劳殖。
我們先看一個簡單的例子铐尚,這個metaclass可以給我們自定義的MyList增加一個add方法:
定義ListMetaclass,按照默認習慣哆姻,metaclass的類名總是以Metaclass結尾宣增,以便清楚地表示這是一個metaclass:
# metaclass是類的模板,所以必須從`type`類型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
有了ListMetaclass矛缨,我們在定義類的時候還要指示使用ListMetaclass來定制類爹脾,傳入關鍵字參數(shù)metaclass
:
class MyList(list, metaclass=ListMetaclass):
pass
當我們傳入關鍵字參數(shù)metaclass
時,魔術就生效了箕昭,它指示Python解釋器在創(chuàng)建MyList時灵妨,要通過ListMetaclass.__new__()
來創(chuàng)建,在此落竹,我們可以修改類的定義泌霍,比如,加上新的方法述召,然后朱转,返回修改后的定義蟹地。
__new__()
方法接收到的參數(shù)依次是:
當前準備創(chuàng)建的類的對象;
類的名字藤为;
類繼承的父類集合锈津;
類的方法集合。
測試一下MyList是否可以調用add()
方法:
>>> L = MyList()
>>> L.add(1)
>> L
[1]
而普通的list沒有add()
方法:
>>> L2 = list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'
動態(tài)修改有什么意義凉蜂?直接在MyList定義中寫上add()
方法不是更簡單嗎?正常情況下性誉,確實應該直接寫窿吩,通過metaclass修改純屬變態(tài)。
但是错览,總會遇到需要通過metaclass修改類定義的纫雁。ORM就是一個典型的例子。
ORM全稱“Object Relational Mapping”倾哺,即對象-關系映射轧邪,就是把關系數(shù)據(jù)庫的一行映射為一個對象,也就是一個類對應一個表羞海,這樣忌愚,寫代碼更簡單,不用直接操作SQL語句却邓。
要編寫一個ORM框架硕糊,所有的類都只能動態(tài)定義,因為只有使用者才能根據(jù)表的結構定義出對應的類來腊徙。
讓我們來嘗試編寫一個ORM框架简十。
編寫底層模塊的第一步,就是先把調用接口寫出來撬腾。比如螟蝙,使用者如果使用這個ORM框架,想定義一個User類來操作對應的數(shù)據(jù)庫表User民傻,我們期待他寫出這樣的代碼:
class User(Model):
# 定義類的屬性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 創(chuàng)建一個實例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到數(shù)據(jù)庫:
u.save()
其中胰默,父類Model和屬性類型StringField、IntegerField是由ORM框架提供的饰潜,剩下的魔術方法比如save()
全部由metaclass自動完成初坠。雖然metaclass的編寫會比較復雜,但ORM的使用者用起來卻異常簡單彭雾。
現(xiàn)在碟刺,我們就按上面的接口來實現(xiàn)該ORM。
首先來定義Field類薯酝,它負責保存數(shù)據(jù)庫表的字段名和字段類型:
class Field(object):
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)
在Field的基礎上半沽,進一步定義各種類型的Field爽柒,比如StringField,IntegerField等等:
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')
下一步者填,就是編寫最復雜的ModelMetaclass了:
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
if name=='Model':
return type.__new__(cls, name, bases, attrs)
print('Found model: %s' % name)
mappings = dict()
for k, v in attrs.items():
if isinstance(v, Field):
print('Found mapping: %s ==> %s' % (k, v))
mappings[k] = v
for k in mappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings # 保存屬性和列的映射關系
attrs['__table__'] = name # 假設表名和類名一致
return type.__new__(cls, name, bases, attrs)
以及基類Model:
class Model(dict, metaclass=ModelMetaclass):
def __init__(self, **kw):
super(Model, self).__init__(**kw)
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.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
當用戶定義一個class User(Model)
時浩村,Python解釋器首先在當前類User的定義中查找metaclass,如果沒有找到占哟,就繼續(xù)在父類Model中查找metaclass心墅,找到了,就使用Model中定義的metaclass的ModelMetaclass來創(chuàng)建User類榨乎,也就是說怎燥,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到蜜暑。
在ModelMetaclass中铐姚,一共做了幾件事情:
排除掉對Model類的修改;
在當前類(比如User)中查找定義的類的所有屬性肛捍,如果找到一個Field屬性隐绵,就把它保存到一個mappings的dict中,同時從類屬性中刪除該Field屬性拙毫,否則依许,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性 );
3.把表名保存到table中恬偷,這里簡化為表名默認為類名悍手。
在Model類中,就可以定義各種操作數(shù)據(jù)庫的方法袍患,比如save()坦康,delete(),find()诡延,update等等滞欠。
我們實現(xiàn)了save()方法,把一個實例保存到數(shù)據(jù)庫中肆良。因為有表名筛璧,屬性到字段的映射和屬性值的集合,就可以構造出INSERT語句惹恃。
編寫代碼試試:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()
輸出如下:
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
可以看到夭谤,save()方法已經(jīng)打印出了可執(zhí)行的SQL語句,以及參數(shù)列表巫糙,只需要真正連接到數(shù)據(jù)庫朗儒,執(zhí)行該SQL語句,就可以完成真正的功能。
小結
metaclass是Python中非常具有魔術性的對象醉锄,它可以改變類創(chuàng)建時的行為乏悄。這種強大的功能使用起來務必小心。