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类的对象时赋值。

原文地址:https://www.cnblogs.com/zwb8848happy/p/8630347.html