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