Python之使用元類MetaClass


  本文參考廖老師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個參數:

  1. class的名稱;
  2. 繼承的父類集合,注意Python支持多重繼承,如果只有一個父類,別忘了tuple的單元素寫法;
  3. 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__()方法接收到的參數依次是:

  1. 當前准備創建的類的對象;

  2. 類的名字;

  3. 類繼承的父類集合;

  4. 類的方法集合。

  測試一下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和屬性類型StringFieldIntegerField是由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,比如StringFieldIntegerField等等:

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中定義的metaclassModelMetaclass來創建User類,也就是說,metaclass可以隱式地繼承到子類,但子類自己卻感覺不到。

ModelMetaclass中,一共做了幾件事情:

  1. 排除掉對Model類的修改;

  2. 在當前類(比如User)中查找定義的類的所有屬性,如果找到一個Field屬性,就把它保存到一個__mappings__的dict中,同時從類屬性中刪除該Field屬性,否則,容易造成運行時錯誤(實例的屬性會遮蓋類的同名屬性);

  3. 把表名保存到__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方法可以執行其他針對數據庫的操作。

  

  

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM