• 12、元类(metaclass)实现精简ORM框架


    metaclass,直译为元类,简单的解释就是:

    先定义metaclass,就可以创建类,最后创建实例。

    所以,metaclass允许你创建类或者修改类。

    先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:

     1 # -*- coding: utf-8 -*-
     2 
     3 # metaclass是创建类,所以必须从`type`类型派生:
     4 class ListMetaclass(type):
     5     def __new__(cls, name, bases, attrs):
     6         attrs['add'] = lambda self, value: self.append(value)
     7         return type.__new__(cls, name, bases, attrs)
     8 
     9 # 指示使用ListMetaclass来定制类
    10 class MyList(list, metaclass=ListMetaclass):
    11     pass
    12 
    13 L = MyList()
    14 L.add(1)
    15 L.add(2)
    16 L.add(3)
    17 L.add('END')
    18 print(L)

    1、先定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这个一个metaclass。metaclass是创建类,所以必须从type类型派生;

    2、有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass。因为MyList是定制化的List,所以需要继承父类list;

    3、__new__()方法接收到的参数依次是:

    (1)当前准备创建的类的对象;

    (2)类的名字;

    (3)类继承的父类集合;

    (4)类的方法合集。

    4、Python和c语言不同,所有的类都是运行时动态创建的。因此创建类MyList时,首先执行该类的定义(第10行至第11行),然后在定义中寻找是否有metaclass,有的话,将参数(name:MyList,bases:list,attrs:系统定义的一些参数)发送至ListMetaclass执行,生成MyList类,同时新定制的类具有add方法。

    ORM

    先贴出廖雪峰Python教程元类的简单ORM框架的代码,然后进行分析:

     1 #!/usr/bin/env python3
     2 # -*- coding: utf-8 -*-
     3 
     4 ' Simple ORM using metaclass '
     5 
     6 class Field(object):
     7 
     8     def __init__(self, name, column_type):
     9         self.name = name
    10         self.column_type = column_type
    11 
    12     def __str__(self):
    13         return '<%s:%s>' % (self.__class__.__name__, self.name)
    14 
    15 class StringField(Field):
    16 
    17     def __init__(self, name):
    18         super(StringField, self).__init__(name, 'varchar(100)')
    19 
    20 class IntegerField(Field):
    21 
    22     def __init__(self, name):
    23         super(IntegerField, self).__init__(name, 'bigint')
    24 
    25 class ModelMetaclass(type):
    26 
    27     def __new__(cls, name, bases, attrs):
    28         if name=='Model':
    29             return type.__new__(cls, name, bases, attrs)
    30         print('Found model: %s' % name)
    31         mappings = dict()
    32         for k, v in attrs.items():
    33             if isinstance(v, Field):
    34                 print('Found mapping: %s ==> %s' % (k, v))
    35                 mappings[k] = v
    36         for k in mappings.keys():
    37             attrs.pop(k)
    38         attrs['__mappings__'] = mappings # 保存属性和列的映射关系
    39         attrs['__table__'] = name # 假设表名和类名一致
    40         return type.__new__(cls, name, bases, attrs)
    41 
    42 class Model(dict, metaclass=ModelMetaclass):
    43 
    44     def __init__(self, **kw):
    45         super(Model, self).__init__(**kw)
    46 
    47     def __getattr__(self, key):
    48         try:
    49             return self[key]
    50         except KeyError:
    51             raise AttributeError(r"'Model' object has no attribute '%s'" % key)
    52 
    53     def __setattr__(self, key, value):
    54         self[key] = value
    55 
    56     def save(self):
    57         fields = []
    58         params = []
    59         args = []
    60         for k, v in self.__mappings__.items():
    61             fields.append(v.name)
    62             params.append('?')
    63             args.append(getattr(self, k, None))
    64         sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
    65         print('SQL: %s' % sql)
    66         print('ARGS: %s' % str(args))
    67 
    68 # testing code:
    69 
    70 class User(Model):
    71     id = IntegerField('id')
    72     name = StringField('username')
    73     email = StringField('email')
    74     password = StringField('password')
    75 
    76 u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
    77 u.save()

    对上面代码进行分析:

    编写底层模块的第一步,就是先把调用接口写出来,然后,根据此接口编写代码实现功能。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码(第70行至第77行):

    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和属性类型IntegerField、StringField是由ORM框架提供的,剩下的魔术方法save()全部由metaclass自动完成。

    注意到User是一个(对应数据库中的表User),其属性是对象(对应数据库表中的字段)。

    虽然metaclass的编写会比较复杂,但ORM的使用者使用起来却异常简单。

    现在,我们就按照上面的接口来实现该ORM。

    首先来定义Field类,它负责保存User类中属性 对应的 数据库表的字段信息(字段名和字段类型)

    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)

    注意: __str__()是Python中有特殊用途的函数,用来定制类。

        如果我们定义了Field类后,打印它的一个实例: print(Field(id, 'bigint')),则会打印出一堆:<__main__.Field object at 0x109afb190>,不好看。

           那么怎样才能打印的好看一点呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了。

           像Field类中的__str__(),打印的信息不仅好看,还能看到实例内部重要的数据。

    在Field的基础上,进一步定义各种类型的Field,比如IntegerField、StringField等等:

    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')

    注意:StringField和IntegerField生成对象,会调用父类Field中的__init__()进行初始化。

    下一步,就是编写复杂的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)

    注意: 元类(metaclass)继承自type,对元类的具体讲述可参考上篇博文(深刻理解Python中的元类(Metaclass))。

    1、首先进行判断,如果将要创建的类是Model,无需做个性化定制,直接通过type创建,排除对Model类的修改;

    2、打印要创建的类的类名;

    3、生成一个Dict对象mappings,保存User类属性和数据库字段的映射关系;

    4、循环读取User类的属性,程序调试后看起来比较直观:

       可以看到User类共有6个属性,其中id、name、email和password属性均是对象(对象保存的是该属性对应的字段信息:字段名和字段类型)。User类的id属性对应数据库表User中的字段(字段名id, 类型'bigint'),name属性对应数据库表User中的字段(字段名username,类型'varchar(100)),email属性对应数据库表User中的字段(字段名email,类型'varchar(100)'),paswword属性对应数据库表User中的字段(字段名password,类型‘varchar(100)')。牢记一点,就如User中的定义一样,key是User类中的属性,value是对应的字段信息(Field类型)。

            在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__的dict中。

    5、在类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性)。

    6、给类添加属性__mappings__,保存的是类属性和数据库字段的对应关系;

    7、给类添加属性__table__,保存的是表名;

    8、动态创建类。

    编写基类Model:

     1 class Model(dict, metaclass=ModelMetaclass):
     2 
     3     def __init__(self, **kw):
     4         super(Model, self).__init__(**kw)
     5 
     6     def __getattr__(self, key):
     7         try:
     8             return self[key]
     9         except KeyError:
    10             raise AttributeError(r"'Model' object has no attribute '%s'" % key)
    11 
    12     def __setattr__(self, key, value):
    13         self[key] = value
    14 
    15     def save(self):
    16         fields = []
    17         params = []
    18         args = []
    19         for k, v in self.__mappings__.items():
    20             fields.append(v.name)
    21             params.append('?')
    22             args.append(getattr(self, k, None))
    23         sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
    24         print('SQL: %s' % sql)
    25         print('ARGS: %s' % str(args))

     注意:动态创建Model时,在类定义中寻找到了metaclass,于是用元类来创建Model。由于在元类中,有判断语句,如果类名时Model,则直接创建,于是不做任何修改生成Modle类。

    结合下面的测试代码来具体分析Model类:

    1 class User(Model):
    2     id = IntegerField('id')
    3     name = StringField('username')
    4     email = StringField('email')
    5     password = StringField('password')
    6 
    7 u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
    8 u.save()

     1、u=User(...)  创建User类的对象,但本身没有构造函数,于是使用父类Model的构造函数__init()进行初始化。

          又因为Model继承自dict,所以super(Model, self).__init__(**kw)使用dict的构造函数进行初始化。

          在Python官方文档中,dict对象的创建这样描述:

         

          dict的创建可以通过:(1)key:value键值对;(2)构造器

       

         所以u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')执行后,u是一个dict类型的对象:

         

          这不仅是User类的一个对象,映射到数据库表User中,即是表中的一行记录。

    2、对象可调用__getattr__和__setattr__方法读取修改属性值,即时对表中记录的某个字段进行读取和修改;

    3、接下来分析save函数:

        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))

    Mysql中insert的用法,举个例子参照下:

    cursor.execute('insert into user (id, name) values (%s, %s)', ['1', 'Michael'])

    fields保存类属性对应的表字段名;

    params保存 ‘?’,在实际中会在特定方法中根据成数据库的类型,被替换成不同的占位符,Mysql的SQL占位符是 %s;

    args保存类属性对应的表一行记录的值。

    所以,我们从始至终一直在强调一个概念:

    创建User类时,定义了类的属性(id,name,email,password),同时映射到对应数据库表中字段的信息,映射关系保存在__mappins__中,这是类的属性!

    根据User类创建对象时,对象的属性值映射到对应数据库表中的一行记录,这是对象的属性!

    所以总结下:User的属性(id,name,email,password):

                       (1)与数据库列名(字段)的映射关系保存在__mappins__中;

                       (2)与数据库一行记录的值,在创建User类的对象时赋值。

  • 相关阅读:
    将指定文件夹下所有图片转换成base64并返回数组
    SQL技巧
    yii 进行事务操作是不可以在一条sql里边放多条sql
    yii 直接执行sql
    按照特定方法排序
    表名为变量时的语法
    如何添加 actions
    触发器原理
    codeCeption 调试方法
    最长不下降子序列(LIS)
  • 原文地址:https://www.cnblogs.com/zwb8848happy/p/8630347.html
Copyright © 2020-2023  润新知