元类即 metaclass,实例的抽象化为类,类的抽象化即元类。
我们通过定义类来创建实例,通过元类来定义类。
自定义List
通过自定义的 listmetaclass 来给自定义的 MyList 类添加一个add方法:
# metaclass是类的模板,所以必须从`type`类型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
class MyList(list, metaclass=ListMetaclass):
pass
>>> L = MyList()
>>> L.add(1)
L
[1]
第4行给对象添加了一个 add() 属性,类似于:
class L(list):
f=lambda self, value: self.append(value)
>>> test=L()
>>> test.f(1)
>>> print(test)
[1]
ORM框架
通过元类 可以动态地修改类定义。
例如ORM,Object Relational Mapping,即对象-关系映射。
通俗来讲,就是对于数据库中的每一个表,都动态定义一个对应的类来操作它。
编写一个ORM框架的流程如下:
确定调用接口
首先我们定义一个元类 Model
,然后从 Model 定义一个 User 类,来操作对应的数据库表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()
name | params | column_type |
---|---|---|
id | 12345 | IntegerField |
name | ‘Michael’ | StringField |
‘test@orm.org’ | StringField | |
password | ‘my-pwd’ | StringField |
Filed 类
首先定义 Filed 类,有2个属性:名称 和 类型 ```python 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)
然后从通用的 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类继承自字典类,但是做了一些增强:
- 定义了
__getattr__
和__setattr__
方法,将字典中的 key-value对 转化成了 attrs-value对。 - 定义了 save方法
创建User类
首先是分别定义不同 Filed 类型的实例:id、name、email、password.class User(Model): # 定义类的属性到列的映射: id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('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类继承自 dict
和 ModelMetaclass
,因此User类可以看做是一个增强了功能的字典类,其初始化方法也是沿用了dict类。
得到的实例 u 是一个字典:
{'email': 'test@orm.org', 'id': 12345, 'name': 'Michael', 'password': 'my-pwd'}
由于Model类中定义了 __getattr__
和 __setattr__
方法,所以既可以用 u[id]
也可以用 u.id
来获取键值。
而且 u 中还存在两个属性: __mappings__
和 __table__
最后调用 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']
问题解惑
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>)]
为什么要将那些属性移动到
__mappings__
中去?
因为User的类属性可能会被u实例的属性覆盖,例如 u.id
对应的值为12345,如果不移走,就会覆盖掉最初 User.id
所对应的的 IntegerField 对象。
- 关于 u 实例的创建
因为 User 类型继承了 Model
类,而 Model
继承了 dict
和 ModelMetaclass
类型,其初始化使用了 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 可以通过传入字典来创建实例。
- Model 中定义的
__getattr__
和__setattr__
方法有什么用?
我个人认为没啥用
- ORM框架用元类写有什么用?
创建新的类时能简单一些,例如不用自己每次都copy一份 save 方法,但这些并不是必要的,至少90%的时候不会用到,所以看不懂的话不必死磕
总结
廖雪峰这个 ORM 框架看得我头都大了,那么它到底干了什么事呢?
- 在创建实例时,直接通过传入字典创建
- 在存入数据库时, User类中未定义的数据不会被保存
- 在元类中定义了save方法,不用每建一个类都自己再定义一遍