元类即 metaclass,实例的抽象化为类,类的抽象化即元类。
我们通过定义类来创建实例,通过元类来定义类。

自定义List

通过自定义的 listmetaclass 来给自定义的 MyList 类添加一个add方法:

  1. # metaclass是类的模板,所以必须从`type`类型派生:
  2. class ListMetaclass(type):
  3. def __new__(cls, name, bases, attrs):
  4. attrs['add'] = lambda self, value: self.append(value)
  5. return type.__new__(cls, name, bases, attrs)
  6. class MyList(list, metaclass=ListMetaclass):
  7. pass
  8. >>> L = MyList()
  9. >>> L.add(1)
  10. L
  11. [1]

第4行给对象添加了一个 add() 属性,类似于:

  1. class L(list):
  2. f=lambda self, value: self.append(value)
  3. >>> test=L()
  4. >>> test.f(1)
  5. >>> print(test)
  6. [1]

ORM框架

通过元类 可以动态地修改类定义。
例如ORM,Object Relational Mapping,即对象-关系映射。
通俗来讲,就是对于数据库中的每一个表,都动态定义一个对应的类来操作它。
编写一个ORM框架的流程如下:

确定调用接口

首先我们定义一个元类 Model ,然后从 Model 定义一个 User 类,来操作对应的数据库表User。
假设我们定义User 类的代码如下:

  1. class User(Model):
  2. # 定义类的属性到列的映射:
  3. id = IntegerField('id')
  4. name = StringField('username')
  5. email = StringField('email')
  6. password = StringField('password')
  7. # 创建一个实例:
  8. u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
  9. # 保存到数据库:
  10. u.save()
name params column_type
id 12345 IntegerField
name ‘Michael’ StringField
email ‘test@orm.org’ StringField
password ‘my-pwd’ StringField

确定整体架构如下: 元类 - 图1

Filed 类

首先定义 Filed 类,有2个属性:名称 类型 元类 - 图2```python class Field(object):

  1. def __init__(self, name, column_type):
  2. self.name = name
  3. self.column_type = column_type
  4. def __str__(self):
  5. return '<%s:%s>' % (self.__class__.__name__, self.name)
然后从通用的 Filed 类定义专门的 StringField 和 IntegerField,<br />需要修改初始化方式,设置类型为 **字符** 和 **整型**。
```python
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)

修改 __new__ 方法:

  • 如果新定义的类为 Model,则不修改
  • 如果是User类,查找其所有类属性,如果属性在 Filed 类中已定义,那么将该属性存到字典 __mappings__ 中,并将该类属性删除
  • 将表名 __table__ 设为类名

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

Model类继承自字典类,但是做了一些增强:

  1. 定义了 __getattr____setattr__ 方法,将字典中的 key-value对 转化成了 attrs-value对。
  2. 定义了 save方法

    创建User类

    class User(Model):
     # 定义类的属性到列的映射:
     id = IntegerField('id')
     name = StringField('username')
     email = StringField('email')
     password = StringField('password')
    
    首先是分别定义不同 Filed 类型的实例:id、name、email、password.
    以 id 为例,其为 IntegerField 类型
  • column_type = "bigint"
  • name = "id"
  • print(id)=<IntegerField:id>

然后是初始化 User 这一实例:
先从 ModelMetaclass 运行 __new__ 方法,此时User中的 attrs.items() 为:

[('__module__', '__main__'), 
 ('__qualname__', 'User'), 
 ('__pydevd_ret_val_dict', {'IntegerField.__init__': None, 
                            'StringField.__init__':  None}), 
 ('id', <__main__.IntegerField object at 0x00000278A5528A30>), 
 ('name', <__main__.StringField object at 0x00000278A6B458B0>), 
 ('email', <__main__.StringField object at 0x00000278A6B45C70>), 
 ('password', <__main__.StringField object at 0x00000278A6B54F10>)]

其中后4项实例属于 Filed 类,将这些属性从 User 中删除,并存到新建的属性 __mappings__ 中。

这是为了避免类属性被实例属性覆盖,举个例子,下面创建的实例u中, u.id 的值为整数12345,但如果没有被覆盖, u.id 应该是一个 IntegerField 类型的对象。

创建实例

# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()

由于User继承了Model类,而Model类继承自 dictModelMetaclass ,因此User类可以看做是一个增强了功能的字典类,其初始化方法也是沿用了dict类。
得到的实例 u 是一个字典:

{'email': 'test@orm.org', 'id': 12345, 'name': 'Michael', 'password': 'my-pwd'}

由于Model类中定义了 __getattr____setattr__ 方法,所以既可以用 u[id] 也可以用 u.id 来获取键值。
而且 u 中还存在两个属性: __mappings____table__
image.png
最后调用 save 方法,得到以下输出:

Found model: User
Found mapping: id ==> <IntegerField:id>
Found mapping: name ==> <StringField:username>
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
SQL: insert into User (id,username,email,password) values (?,?,?,?)
ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd']

问题解惑

  1. ModelMetaclass 中的 User类的 attrs 有哪些?

    [('module', 'main'),
    ('qualname', 'User'),
    ('pydevd_ret_val_dict', {'IntegerField.init__': None,
    'StringField.init':  None}),
    ('id', <main.IntegerField object at 0x00000278A5528A30>),
    ('name', <main.StringField object at 0x00000278A6B458B0>),
    ('email', <main.StringField object at 0x00000278A6B45C70>),
    ('password', <main.StringField object at 0x00000278A6B54F10>)]
    
  2. 为什么要将那些属性移动到 __mappings__ 中去?

因为User的类属性可能会被u实例的属性覆盖,例如 u.id 对应的值为12345,如果不移走,就会覆盖掉最初 User.id 所对应的的 IntegerField 对象。

  1. 关于 u 实例的创建

因为 User 类型继承了 Model 类,而 Model 继承了 dictModelMetaclass 类型,其初始化使用了 super(Model, self).__init__(**kw) 方法,Python会根据 MRO 顺序来寻找初始化方法:

>>> User.mro

(<class 'main.User'>, <class 'main.Model'>, <class 'dict'>, <class 'object'>)

也就是说,当 User 类中未定义 __init__ 方法时,python会去 Model 中寻找,Model 中没有时,会继续去 dict 中寻找,因此 u 可以通过传入字典来创建实例。

  1. Model 中定义的 __getattr____setattr__ 方法有什么用?

我个人认为没啥用

  1. ORM框架用元类写有什么用?

创建新的类时能简单一些,例如不用自己每次都copy一份 save 方法,但这些并不是必要的,至少90%的时候不会用到,所以看不懂的话不必死磕

总结

廖雪峰这个 ORM 框架看得我头都大了,那么它到底干了什么事呢?

  1. 在创建实例时,直接通过传入字典创建
  2. 在存入数据库时, User类中未定义的数据不会被保存
  3. 在元类中定义了save方法,不用每建一个类都自己再定义一遍