zoukankan      html  css  js  c++  java
  • 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方法可以执行其他针对数据库的操作。

      

      

      

  • 相关阅读:
    项目中常用的linux命令
    Flutter移动电商实战 --(12)首页导航区域编写
    Flutter移动电商实战 --(13)ADBanner组件的编写
    Flutter移动电商实战 --(10)使用FlutterSwiper制作轮播效果
    Flutter移动电商实战 --(9)移动商城数据请求实战
    Flutter移动电商实战 --(8)dio基础_伪造请求头获取数据
    Flutter移动电商实战 --(7)dio基础_POST请求的使用
    Flutter移动电商实战 --(6)dio基础_Get_Post请求和动态组件协作
    Flutter移动电商实战 --(5)dio基础_引入和简单的Get请求
    Flutter移动电商实战 --(4)打通底部导航栏
  • 原文地址:https://www.cnblogs.com/minseo/p/15572543.html
Copyright © 2011-2022 走看看