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方法可以执行其他针对数据库的操作。

  

  

  

原文地址:https://www.cnblogs.com/minseo/p/15572543.html