本文參考廖老師Python教程:https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072#0
說明:廖老師Python教程使用元類這節中說道metaclass是Python面向對象最難連接,也是最難使用的魔術代碼。正常情況下,你不會碰到需要使用metaclass的情況。
當時看不懂就直接跳過這節了,但在學到實戰的時候又需要使用metaclass來說實現ORM,又回過頭來學習。
本文盡量詳細解釋使用metaclass實現ORM的過程。
使用元類
type()
動態語言和靜態語言最大的不同,就是函數和類的定義,不是編譯時定義的,而是運行時動態創建的。
比方說我們要定義一個Hello的class,就寫一個hello.py的模塊
D:\learn-python3\面向對象高級編程\使用原類\hello.py
# 定義Hello類
class Hello(object):
# 定義類函數,該函數傳遞一個參數name然后打印,name設置默認值
def hello(self,name='world'):
print('Hello,%s.' % name)
當Python解釋器載入hello模塊時,就會依次執行該模塊的所有語句,執行結果就是動態創建出一個Hello的class對象,測試如下:
# 測試Hello類 start # 導入類,本python文件和定義class的文件hello.py在同一個文件夾下 from hello import Hello # 實例化類得到實例h h = Hello() # 執行類方法,打印,因為name有默認值所以可以不傳遞 h.hello() # Hello,world. # 打印Hello的type屬性,是一個type類 print(type(Hello)) # <class 'type'> # 打印h的type屬性 print(type(h)) # <class 'hello.Hello'> # 測試Hello類 end
type()函數可以查看一個類型或變量的類型,Hello是一個class,它的類似就是type,而h是一個實例,它的類型就是class Hello。
我們說class的定義是運行時動態創建的,而創建class的方法就是使用type()函數。
type()函數既可以返回一個對象的類型,又可以創建出新的類型,比如,我們可以通過type()函數創建出Hello類,而無需通過class Hello(object)...的定義:
# 通過type()函數創建類 start
# 先定義函數,然后再使用type創建類的時候把整個函數綁定到類的函數hello
def fn(self,name='world'):
print('Hello,%s.' % name)
# 創建Hello class
# type()函數需要傳遞3個參數
# 1,class的名稱,本次為Hello
# 2,繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘啦tuple的單元素寫法,就是寫一個父類然后加符號,
# 3,class的方法名稱與函數綁定,這里我們把函數fn綁定到方法名hello上
Hello = type('Hello',(object,),dict(hello=fn))
# 實例化
h = Hello()
# 執行類的方法
h.hello()
# Hello,world.
print(type(Hello))
# <class 'type'>
print(type(h))
# <class '__main__.Hello'>
# 通過type()函數創建類 end
要創建一個class對象,type()函數依次傳入3個參數:
- class的名稱;
- 繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
- class的方法名稱與函數綁定,這里我們把函數
fn綁定到方法名hello上。
通過type()函數創建的類和直接寫class是完全一樣的,因為Python解釋器遇到class定義時,僅僅是掃描一下class定義的語法,然后調用type()函數創建出class。
正常情況下,我們都用class Xxx...來定義類,但是,type()函數也允許我們動態創建出類來,也就是說,動態語言本身支持運行期動態創建類,這和靜態語言有非常大的不同,要在靜態語言運行期創建類,必須構造源代碼字符串再調用編譯器,或者借助一些工具生成字節碼實現,本質上都是動態編譯,會非常復雜。
metaclass
除了使用type()動態創建類之外,要控制類的創建形象,還可以使用metaclass。
metaclass,直譯為元類,簡單的解釋就是:
當我們定義了類以后,就可以根據這個類創建出實例,所以:先定義類,然后創建實例。
但是如果我們想要創建出類呢?那就必須根據metaclass創建出類,所以:先定義metaclass,然后創建類。
連接起來就是:先定義metaclass,就可以創建類,最后創建實例。
所以,metaclass允許你創建類或者修改類。換句話說,你可以把類看出是metaclass創建處理的“實例”。
metaclass是Python面向對象里最難理解,也是最難使用的魔術代碼。正常情況下,你不會碰到需要使用metaclass的情況,所以,以下內容看不懂也沒關系,因為基本上你不會用到。
我們先看一個簡單的例子,這個metaclass可以給我們自定義的MyList增加一個add方法:
定義ListMetaclass,按照默認習慣,metaclass的類名總是以Metaclass結尾,以便清楚地表示這是一個metaclass:
use_metaclass.py
# metaclass是類的模板,所以必須從type類型派生
class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
# 增加一個方法add綁定的函數是一個匿名函數,該函數執行的操作是往listappend一個元素value
attrs['add'] = lambda self,value:self.append(value)
return type.__new__(cls,name,bases,attrs)
匿名函數簡化代碼代碼不容易理解,以下直接定義一個函數然后再綁定的方法更容易理解,和上面使用type函數添加方法的例子類似
def add(self,value):
print(self)
self.append(value)
attrs['add'] = add
有了ListMetaclass,我們在定義類的時候還要指示使用ListMetaclass來定制類,傳入關鍵字參數metaclass:
class MyList(list,metaclass=ListMetaclass):
pass
當我們傳入關鍵字參數metaclass時,魔術就生效了,它指示Python解釋器在創建MyList時,要通過ListMetaclass.__new__()來創建,在此,我們可以修改類的定義,比如,加上新的方法,然后,返回修改后的定義。
__new__()方法接收到的參數依次是:
-
當前准備創建的類的對象;
-
類的名字;
-
類繼承的父類集合;
-
類的方法集合。
測試一下MyList是否可以調用add()方法:
L = MyList() L.add(1) print(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'
動態修改有什么意義?直接在MyList定義中寫上add()方法不是更簡單嗎?正常情況下,確實應該直接寫,通過metaclass修改純屬變態。
直接在MyList中添加add()方法代碼如下
test.py
# 正常方法在MyList添加add方法 start
class MyList(list):
def add(self,value):
self.append(value)
# 實例化,相當於創建了一個空的list
L = MyList()
# 調用類的add方法,相當於執行了類的內部函數add(self,value)
# 傳遞的參數為self即本身這個空的list可以省略,value為1
# 執行add方法相當於執行了L.append(1)往list添加一個元素1
L.add(1)
# 空list使用了append方法添加一個元素1打印list為包含一個1個元素的list
print(L)
# [1]
# 常方法在MyList添加add方法 end
使用metaclass元類創建的新類到底執行了上面操作,下面我們通過打印__new__()函數的幾個參數來分析
修改代碼
# metaclass是類的模板,所以必須從type類型派生
class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
print('參數cls為: %s' % cls)
print('參數name為: %s' % name)
print('參數bases為: %s' % bases)
# 增加add方法前打印attrs
print('參數attrs為: %s' % attrs)
# 以下添加方法和匿名函數效果一樣
# def add(self,value):
# print(self)
# self.append(value)
# attrs['add'] = add
# 增加一個方法add綁定的函數是一個匿名函數,該函數執行的操作是往listappend一個元素value
attrs['add'] = lambda self,value:self.append(value)
# 增加add方法后打印attrs
print('增加add方法后參數attrs為: %s' % attrs)
return type.__new__(cls,name,bases,attrs)
class MyList(list,metaclass=ListMetaclass):
pass
L = MyList()
L.add(1)
print(L)
# [1]
執行輸出如下
參數cls為: <class '__main__.ListMetaclass'>
參數name為: MyList
參數bases為: <class 'list'>
參數attrs為: {'__module__': '__main__', '__qualname__': 'MyList'}
增加add方法后參數attrs為: {'__module__': '__main__', '__qualname__': 'MyList', 'add': <function ListMetaclass.__new__.<locals>.<lambda> at 0x0000016B0D9BE288>}
[1]
很明顯函數__new__()對應的4個參數
# 當前准備創建類的對象即當前類使用那一個MetaClass類來創建,本次是使用ListMetaclass
參數cls為: <class '__main__.ListMetaclass'>
# 當前創建的類的名字MyList即當前需要使用MetaCLass來創建的類,即在定義類時使用了關鍵字參數metaclass
參數name為: MyList
# 類繼承的父類集合,本次繼承的父類為list
參數bases為: <class 'list'>
# 類的方法集合,默認在沒有任何修改時包含兩個方法
參數attrs為: {'__module__': '__main__', '__qualname__': 'MyList'}
# 在__new__內部添加了一個新方法add任何把添加方法后的新類返回了
增加add方法后參數attrs為: {'__module__': '__main__', '__qualname__': 'MyList', 'add': <function ListMetaclass.__new__.<locals>.<lambda> at 0x0000016B0D9BE288>}
對應的關系圖示如下

個人理解:感覺元類像一個裝飾器,把一個類裝飾以后再返回一個新的類。
下面通過調試模式看一遍執行過程



















動態修改有什么意義?直接在MyList定義中寫上add()方法不是更簡單嗎?正常情況下,確實應該直接寫,通過metaclass修改純屬變態。
但是,總會遇到需要通過metaclass修改類定義的。ORM就是一個典型的例子。
ORM全稱“Object Relational Mapping”,即對象-關系映射,就是把關系數據庫的一行映射為一個對象,也就是一個類對應一個表,這樣,寫代碼更簡單,不用直接操作SQL語句。
要編寫一個ORM框架,所有的類都只能動態定義,因為只有使用者才能根據表的結構定義出對應的類來。
讓我們來嘗試編寫一個ORM框架。
編寫底層模塊的第一步,就是先把調用接口寫出來。比如,使用者如果使用這個ORM框架,想定義一個User類來操作對應的數據庫表User,我們期待他寫出這樣的代碼:
class User(Model):
# 定義類的屬性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 創建一個實例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到數據庫:
u.save()
其中,父類Model和屬性類型StringField、IntegerField是由ORM框架提供的,剩下的魔術方法比如save()全部由父類Model自動完成。雖然metaclass的編寫會比較復雜,但ORM的使用者用起來卻異常簡單。
現在,我們就按上面的接口來實現該ORM。
首先來定義Field類,它負責保存數據庫表的字段名和字段類型:
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,如果沒有找到,就繼續在父類Model中查找metaclass,找到了,就使用Model中定義的metaclass的ModelMetaclass來創建User類,也就是說,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到。
在ModelMetaclass中,一共做了幾件事情:
-
排除掉對
Model類的修改; -
在當前類(比如
User)中查找定義的類的所有屬性,如果找到一個Field屬性,就把它保存到一個__mappings__的dict中,同時從類屬性中刪除該Field屬性,否則,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性); -
把表名保存到
__table__中,這里簡化為表名默認為類名。
在Model類中,就可以定義各種操作數據庫的方法,比如save(),delete(),find(),update等等。
我們實現了save()方法,把一個實例保存到數據庫中。因為有表名,屬性到字段的映射和屬性值的集合,就可以構造出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()方法已經打印出了可執行的SQL語句,以及參數列表,只需要真正連接到數據庫,執行該SQL語句,就可以完成真正的功能。
可以看到執行save()方法輸出了實現插入數據庫的語句,只要建立了連接池執行以下類似語句
cur.execute(sql,args)
其中sql是執行的語句,在sql內可以使用%s進行類似格式化的替換,替換的內容為args列表內的元素
使用異步連接MySQL執行sql語句的用法可參考:https://www.cnblogs.com/minseo/p/15538636.html
不到100行代碼,我們就通過metaclass實現了一個精簡的ORM框架,是不是非常簡單?
以上代碼順序有點亂,下面貼出全部代碼
orm.py
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()
print(attrs)
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 # 假設表名和類名一致
print(attrs)
return type.__new__(cls, name, bases, attrs)
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 = []
print(self)
for k, v in self.__mappings__.items():
print(k,v)
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 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)
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 User(Model):
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()

下面拆分來分析執行過程
首先我們看一下整個類的關系拓撲圖

其中StringField和IntergerField繼承至類Field ,User繼承類Model,Model類定義了元類方法metaclass,所以User類在創建的時候首先會在自己定義的參數查找metaclass沒有找到,去父類找metaclass找到了,所以User類也會使用MoelMedaClass進行創建。
在整體分析執行過程之前我們來拆分解析
1,拆分解析類User的創建過程
test.py
看以下代碼,為了方便解析,我們定義了字段類Field然后又分別定義了字符串類StringField和整數字段類IntergerField他們都繼承了類Filed
然后我們定義了User的父類Model類,繼承dict類,這里我們沒有設置關鍵字參數metaclass,所以不會執行重新創建類的步驟
接下來我們定義了User類繼承了Model,即相當於繼承dict,如果在User類內部沒有定義任何屬性和方法則User類其實就是一個dict類
我們在類的內部定義了4個屬性分別是id name email password他們對應的值則是實例化以后的一個實例
例如屬性id對應的是一個通過IntegerField類實例化以后的實例
# 拆分解析類User start
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回實例對象的時候好看一點默認返回為 <__main__.StringField object at 0x0000025CC313EF08>
# 定義了__str__返回為 <StringField:email>
# 可以省略使用默認也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定義字符串類繼承至Field
class StringField(Field):
def __init__(self,name):
# 繼承父類的初始化方法
# Python3可以省略參數(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
# 拆分解析Model沒有定義metaclass關鍵字參數,只是繼承了類dict
class Model(dict):
pass
# 定義類User繼承Model所以User繼承了dict的方法和屬性
class User(Model):
# 除了dict的方法和屬性,User添加一下屬性
# 該屬性的key即為id,name,email,password對應的值則為實例化以后的實例
# 例如屬性id對應的就是通過類IntegerField()傳遞參數'id'生成的實例
# 該實例繼承至類Field,類Field在初始化的時候定義了兩個屬性name和column_type
# 分別代表的就是數據庫表里對應字段名稱和字段類型 本次字段名為'id',字段屬性為'bigint'長整數
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 實例化,因為User繼承了字典,所以可以以鍵值對的方式賦值
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 打印實例u,其實就是一個字典
print(u)
# {'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
# 但是這個字典除了字典有的所有方法以外還繼承了類User定義的幾個屬性 id,name,emai,password
# 下面遍歷打印出這幾個屬性,這幾個屬性是從類User繼承的,所以如果把u修改為類User打印結果也是一樣的
for i in dir(u):
if i in ['id','name','email','password']:
print("%s:%s" % (i,getattr(u,i,None)))
# email:<StringField:email>
# id:<IntegerField:id>
# name:<StringField:username>
# password:<StringField:password>
# 拆分解析類User end
輸出如下
{'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
email:<StringField:email>
id:<IntegerField:id>
name:<StringField:username>
password:<StringField:password>
下面通過調試模式分析執行過程

















省略重復的幾個步驟,分別又往類User添加了屬性name,email,password





遍歷完所有實例u的屬性,然后本次只打印了對應個新增的4個屬性

補充:getattr使用方法
getattr(object,i,None) # 其中object是一個對象 # i為去對象中查找對應i屬性,如果有對應屬性則把屬性值返回 # None 如果沒有對應屬性則返回None
修改代碼我們可以打印對應實例分別對應的屬性name和colume_tpye
for i in dir(u):
if i in ['id','name','email','password']:
# print("%s:%s" % (i,getattr(u,i,None)))
print("%s:%s" % (getattr(u,i,None).name,getattr(u,i,None).column_type))
輸出如下
email:varchar(100) id:bigint username:varchar(100) password:varchar(100)
2,拆分分析類User的內部屬性
首先我們在定義的metacalss里面把類原樣返回不進行任何修改,代碼如下
test.py
# 拆分解析添加關鍵字參數metaclass但是不對類進行修改 start
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回實例對象的時候好看一點默認返回為 <__main__.StringField object at 0x0000025CC313EF08>
# 定義了__str__返回為 <StringField:email>
# 可以省略使用默認也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定義字符串類繼承至Field
class StringField(Field):
def __init__(self,name):
# 繼承父類的初始化方法
# Python3可以省略參數(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果類是Model則不做任何修改,類原樣返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否則執行對類的重新定義,本次沒有定義,還是原樣返回
print('參數cls為: %s' % cls)
print('參數name為: %s' % name)
print('參數bases為: %s' % bases)
# 增加add方法前打印attrs
print('參數attrs為: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
pass
# 定義類User繼承Model所以User繼承了dict的方法和屬性
class User(Model):
# 除了dict的方法和屬性,User添加一下屬性
# 該屬性的key即為id,name,email,password對應的值則為實例化以后的實例
# 例如屬性id對應的就是通過類IntegerField()傳遞參數'id'生成的實例
# 該實例繼承至類Field,類Field在初始化的時候定義了兩個屬性name和column_type
# 分別代表的就是數據庫表里對應字段名稱和字段類型 本次字段名為'id',字段屬性為'bigint'長整數
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 實例化,因為User繼承了字典,所以可以以鍵值對的方式賦值
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 打印實例u,其實就是一個字典
print(u)
# {'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
# 但是這個字典除了字典有的所有方法以外還繼承了類User定義的幾個屬性 id,name,emai,password
# 下面遍歷打印出這幾個屬性,這幾個屬性是從類User繼承的,所以如果把u修改為類User打印結果也是一樣的
# getarrt方法從對象中獲取對應屬性,如果對象包含該屬性則返回屬性值,如果不包含對應屬性則返回None
for i in dir(u):
if i in ['id','name','email','password']:
print("%s:%s" % (i,getattr(u,i,None)))
# print("%s:%s" % (getattr(u,i,None).name,getattr(u,i,None).column_type))
# email:<StringField:email>
# id:<IntegerField:id>
# name:<StringField:username>
# password:<StringField:password>
# 拆分解析添加關鍵字參數metaclass但是不對類進行修改 end
從代碼我們可以看到我們給類Model添加了關鍵字參數metaclass=ModelMetaclass使用ModelMetaclass對類進行重新創建
我們不需要修改類Model只需要修改類User,以下代碼排除對Model的修改
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
當創建到類User時,首先在類User里面找metaclass結果沒有找到,就去它的父類查找有沒有關鍵字參數metaclass找到了,所以使用定義的類ModelMetaclass對類User進行重新創建,但是本次代碼我們只是打印了對應的幾個參數並沒有修改,相當於類還是原樣返回了。
運行輸出如下
參數cls為: <class '__main__.ModelMetaclass'>
參數name為: User
參數bases為: <class '__main__.Model'>
參數attrs為: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x0000016D9A026BC8>, 'name': <__main__.StringField object at 0x0000016D9A026C08>, 'email': <__main__.StringField object at 0x0000016D9A026C48>, 'password': <__main__.StringField object at 0x0000016D9A026C88>}
{'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
email:<StringField:email>
id:<IntegerField:id>
name:<StringField:username>
password:<StringField:password>
我們重點看輸出的attrs除了固定的兩個屬性
'__module__': '__main__', '__qualname__': 'User'
多出的幾個屬性就是在類User里面定義的4個屬性,
'id': <__main__.IntegerField object at 0x0000016D9A026BC8>, 'name': <__main__.StringField object at 0x0000016D9A026C08>, 'email': <__main__.StringField object at 0x0000016D9A026C48>, 'password': <__main__.StringField object at 0x0000016D9A026C88>
3,拆分使用metaclass對類User重新創建
通過上面的例子在ModelMetaclass內沒有對類User進行修改重新創建,只是輸出了類User的__attr__屬性,我們可以看到除了默認的兩個屬性以為,類User多出來的幾個屬性分別為id,name,email,password他們對應的值為實例化以后的一個實例。
下面我們來分析一個MySQL的插入語句,看一下我們需要哪些參數,以及我們是否可以通過查找User對應的__attr__找到這些參數
假如我們要往表里插入一條數據,應該使用以下語句
insert into user (id,username,emai,password) values (12345,'Michael','test@orm.org','my-pwd')
插入語句的格式為
insert into # 固定格式 user # 表名 (id,username,emai,password) # 字段名 values # 固定格式 (12345,'Michael','test@orm.org','my-pwd') # 字段的值
除了固定格式的insert into和values,我們需要從User對應的__attr__中提取的內容有表名,字段名,字段的值
其中表名我們可以定義為和類名一樣本次為user,可以通過函數__new__(cls, name, bases, attrs)的參數name獲取到name=‘User’
字段名(id,username,emai,password)的獲取,例如我想要獲得字段id則可以通過實例u的id屬性獲得一個StringField實例,然后再通過這個實例的name屬性獲得對應的字段名
通過實例打印對應的4個字段名
print(u.id.name,u.name.name,u.email.name,u.password.name) # id username email password
解析:u.id為對應的實例IntegerField('id'),然后再使用屬性name則可以獲得字段名。
字段的值(12345,'Michael','test@orm.org','my-pwd') 我們可以直接從實例化后的字典中通過key獲取
print(u['id'],u['name'],u['email'],u['password']) # 12345 Michael test@orm.org my-pwd
但是通過key獲取只能是使用字典的dict[key]方式來獲取,如果想要通過屬性的方式獲取例如u.id或getattr(u,'id',None)獲取到的是類的id屬性即IntegerField('id')實例
示例如下
print(u.id,u.name,u.email,u.password) print(getattr(u,'id',None),getattr(u,'name',None),getattr(u,'email',None),getattr(u,'password',None))
上面兩種方式取獲取實例u的id屬性效果的一樣的,獲取到的都是對應的實例,而不是我們想要的字段值12345 Michael test@orm.org my-pwd
<IntegerField:id> <StringField:username> <StringField:email> <StringField:password> <IntegerField:id> <StringField:username> <StringField:email> <StringField:password>
補充:字典實例想要通過key去獲取該可以對應的值有兩種方式
①使用dict[key]方法獲取,這個是字典自帶的最常用的方法
②使用dict.key屬性方式獲取,需要在類里面定義__getattr__(),如果沒有定義__getattr__方法則字典類型的實例是無法通過屬性去獲取值的
示例如下
class Dict(dict):
# 需要定義__getattr__方法才能通過屬性獲得值
def __getattr__(self,key):
return self[key]
d = Dict(id=456,name='李四')
print(d.id)
輸出為
456
假如類定義了相同的屬性則使用屬性獲取會獲得類的屬性
class Dict(dict):
id = 123
# 需要定義__getattr__方法才能通過屬性獲得值
def __getattr__(self,key):
return self[key]
d = Dict(id=456,name='李四')
print(d.id)
輸出為得到的是類的屬性值不是示例的屬性值
123
需要得到實例的對應屬性值只能是通過字典的方法
print(d['id'])
假如我們現在想要通過實例的屬性來獲取實例的值,即我想通過u.id獲取的值和u['id']的到的值是一樣的,應該怎么辦?
我們可以把原始的__attr__里面對應的4個屬性id,name,email,password提取出來組成一個字典,然后把這個字典作為一個value,重新定義一個key為'__mappings__'用於存儲這個字典組成的value,然后再把原__attr__里面的這4個屬性刪除掉,這樣實例u的屬性和類User的屬性就不會沖突了,定義好__getattr__方法就可以使用屬性的方法去獲得字段對應的值。
修改后的代碼如下
# 使用metaclass對類進行修改 start
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回實例對象的時候好看一點默認返回為 <__main__.StringField object at 0x0000025CC313EF08>
# 定義了__str__返回為 <StringField:email>
# 可以省略使用默認也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定義字符串類繼承至Field
class StringField(Field):
def __init__(self,name):
# 繼承父類的初始化方法
# Python3可以省略參數(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果類是Model則不做任何修改,類原樣返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否則執行對類的重新定義,本次沒有定義,還是原樣返回
print('參數cls為: %s' % cls)
print('參數name為: %s' % name)
print('參數bases為: %s' % bases)
# 增加add方法前打印attrs
print('參數attrs為: %s' % attrs)
# 定義一個空字典,用於存儲對應的屬性和值
mappings = {}
# 遍歷attr字典,如果對應的v是類Field的子集則安裝key的方法存儲到字典中
for k,v in attrs.items():
if isinstance(v,Field):
mappings[k] = v
# 原attrs刪除對應的屬性
for k in mappings:
attrs.pop(k)
attrs['__mappings__'] = mappings
attrs['__tabel__'] = name
print('修改后參數attrs為: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
pass
# 定義類User繼承Model所以User繼承了dict的方法和屬性
class User(Model):
# 除了dict的方法和屬性,User添加一下屬性
# 該屬性的key即為id,name,email,password對應的值則為實例化以后的實例
# 例如屬性id對應的就是通過類IntegerField()傳遞參數'id'生成的實例
# 該實例繼承至類Field,類Field在初始化的時候定義了兩個屬性name和column_type
# 分別代表的就是數據庫表里對應字段名稱和字段類型 本次字段名為'id',字段屬性為'bigint'長整數
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 使用metaclass對類進行修改 end
運行輸出如下
參數cls為: <class '__main__.ModelMetaclass'>
參數name為: User
參數bases為: <class '__main__.Model'>
參數attrs為: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x000002C02B419B48>, 'name': <__main__.StringField object at 0x000002C02B419B88>, 'email': <__main__.StringField object at 0x000002C02B419BC8>, 'password': <__main__.StringField object at 0x000002C02B419C08>}
修改后參數attrs為: {'__module__': '__main__', '__qualname__': 'User', '__mappings__': {'id': <__main__.IntegerField object at 0x000002C02B419B48>, 'name': <__main__.StringField object at 0x000002C02B419B88>, 'email': <__main__.StringField object at 0x000002C02B419BC8>, 'password': <__main__.StringField object at 0x000002C02B419C08>}, '__tabel__': 'User'}
我們對比修改前的attrs和修改后的attrs有什么不同
①把User對應的4個屬性重新放到一個字典中,並且創建對應的key為'__mappings__'
②使用pop方法刪除了原有的4個屬性,如果不刪除則還是會沖突
③往attrs添加一個字段'__table__'用於存儲表名,這里我們假設數據庫的表名就是類名,當前創建的類名可以通過參數name獲取到

4,在父類Model中創建save()方法
使用metaclass對類User的修改已經完成下面我們在User的父類創建一個save()方法用於執行MySQL的insert語句即插入語句
我們知道使用MySQL連接池創建浮標然后執行sql語句的格式為
cur.execute(sql, args)
其中sql為需要執行的sql語句,args為帶的參數,例如我們要往數據庫的表user中插入一條數據執行方式為
sql = 'insert into user (id,username,email) values(%s,%s,%s,%s)' args = [12345,'Michael','test@orm.org','my-pwd'] cur.excute(sql,args)
下面我們在save()方法中去獲取對應的參數,代碼如下
# 在類Model中定義save()方法 start
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回實例對象的時候好看一點默認返回為 <__main__.StringField object at 0x0000025CC313EF08>
# 定義了__str__返回為 <StringField:email>
# 可以省略使用默認也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定義字符串類繼承至Field
class StringField(Field):
def __init__(self,name):
# 繼承父類的初始化方法
# Python3可以省略參數(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果類是Model則不做任何修改,類原樣返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否則執行對類的重新定義,本次沒有定義,還是原樣返回
print('參數cls為: %s' % cls)
print('參數name為: %s' % name)
print('參數bases為: %s' % bases)
# 增加add方法前打印attrs
print('參數attrs為: %s' % attrs)
# 定義一個空字典,用於存儲對應的屬性和值
mappings = {}
# 遍歷attr字典,如果對應的v是類Field的子集則安裝key的方法存儲到字典中
for k,v in attrs.items():
if isinstance(v,Field):
mappings[k] = v
# 原attrs刪除對應的屬性
for k in mappings:
attrs.pop(k)
attrs['__mappings__'] = mappings
attrs['__tabel__'] = name
print('修改后參數attrs為: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
# 定義__getattr__方法,改方法傳遞一個key值然后使用字典的取值方式返回
# 不定的這個方法無法使用屬性的方式獲取值
def __getattr__(self,key):
return self[key]
def save(self):
# 定義空list用於存儲字段名稱
fields = []
# 定義空list用於存儲占位符'?'
params = []
# 定義空list用於存儲字段的值
args = []
# 使用k,v的方式遍歷k為對應的屬性如id v為對應的實例
for k,v in self.__mappings__.items():
# print(k,v)
# 通過實例的屬性獲取字段名
fields.append(v.name)
# 通過屬性從實例獲取到對應字段的值
# 需要定義__getattr__方法,這里不能使用self.k這種方法來獲取,因為使用這種方法k是作為一個屬性值而不是變量
args.append(getattr(self,k,None))
# 每增加一個字段則增加一個占位符?
params.append('?')
print(fields)
print(args)
# 使用join方法把list拼接成str
sql = 'insert into %s (%s) values (%s)' %(self.__tabel__,','.join(fields),','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
# 定義類User繼承Model所以User繼承了dict的方法和屬性
class User(Model):
# 除了dict的方法和屬性,User添加一下屬性
# 該屬性的key即為id,name,email,password對應的值則為實例化以后的實例
# 例如屬性id對應的就是通過類IntegerField()傳遞參數'id'生成的實例
# 該實例繼承至類Field,類Field在初始化的時候定義了兩個屬性name和column_type
# 分別代表的就是數據庫表里對應字段名稱和字段類型 本次字段名為'id',字段屬性為'bigint'長整數
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 實例化
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 執行save()方法,本次只是模擬執行MySQL的語句,並沒有真正執行MySQL語句
u.save()
# 在類Model中定義save()方法 end
執行輸出如下
參數cls為: <class '__main__.ModelMetaclass'>
參數name為: User
參數bases為: <class '__main__.Model'>
參數attrs為: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x0000022A0DCCDFC8>, 'name': <__main__.StringField object at 0x0000022A0DCD4048>, 'email': <__main__.StringField object at 0x0000022A0DCD4088>, 'password': <__main__.StringField object at 0x0000022A0DCD40C8>}
修改后參數attrs為: {'__module__': '__main__', '__qualname__': 'User', '__mappings__': {'id': <__main__.IntegerField object at 0x0000022A0DCCDFC8>, 'name': <__main__.StringField object at 0x0000022A0DCD4048>, 'email': <__main__.StringField object at 0x0000022A0DCD4088>, 'password': <__main__.StringField object at 0x0000022A0DCD40C8>}, '__tabel__': 'User'}
['id', 'username', 'email', 'password']
[12345, 'Michael', 'test@orm.org', 'my-pwd']
SQL: insert into User (id,username,email,password) values (?,?,?,?)
ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd']
我們可以看到執行save()方法把我們所需要的參數都獲取到了,實際如果連接了數據庫則可以執行相應的插入操作,同理通過定義select,update,delete方法可以執行其他針對數據庫的操作。
