本文參考廖老師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方法可以執行其他針對數據庫的操作。