Python之使用元类MetaClass
本文参考廖老师Python教程:https://www.liaoxuefeng.com/wiki/1016959663602400/1017592449371072#0
说明:廖老师Python教程使用元类这节中说道metaclass是Python面向对象最难连接,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况。
当时看不懂就直接跳过这节了,但在学到实战的时候又需要使用metaclass来说实现ORM,又回过头来学习。
本文尽量详细解释使用metaclass实现ORM的过程。
使用元类
type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个Hello的class,就写一个hello.py的模块
D:\learn-python3\面向对象高级编程\使用原类\hello.py
# 定义Hello类class Hello(object):
# 定义类函数,该函数传递一个参数name然后打印,name设置默认值
def hello(self,name='world'):
print('Hello,%s.' % name)
当Python解释器载入hello
模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello
的class对象,测试如下:
# 测试Hello类 start# 导入类,本python文件和定义class的文件hello.py在同一个文件夹下
from hello import Hello
# 实例化类得到实例h
h = Hello()
# 执行类方法,打印,因为name有默认值所以可以不传递
h.hello()
# Hello,world.
# 打印Hello的type属性,是一个type类
print(type(Hello))
# <class 'type'>
# 打印h的type属性
print(type(h))
# <class 'hello.Hello'>
# 测试Hello类 end
type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类似就是type,而h是一个实例,它的类型就是class Hello。
我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。
type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:
# 通过type()函数创建类 start# 先定义函数,然后再使用type创建类的时候把整个函数绑定到类的函数hello
def fn(self,name='world'):
print('Hello,%s.' % name)
# 创建Hello class
# type()函数需要传递3个参数
# 1,class的名称,本次为Hello
# 2,继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘啦tuple的单元素写法,就是写一个父类然后加符号,
# 3,class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上
Hello = type('Hello',(object,),dict(hello=fn))
# 实例化
h = Hello()
# 执行类的方法
h.hello()
# Hello,world.
print(type(Hello))
# <class 'type'>
print(type(h))
# <class '__main__.Hello'>
# 通过type()函数创建类 end
要创建一个class对象,type()
函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数
fn
绑定到方法名hello
上。
通过type()
函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()
函数创建出class。
正常情况下,我们都用class Xxx...
来定义类,但是,type()
函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
metaclass
除了使用type()动态创建类之外,要控制类的创建形象,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想要创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看出是metaclass创建处理的“实例”。
metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。
我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add
方法:
定义ListMetaclass
,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
use_metaclass.py
# metaclass是类的模板,所以必须从type类型派生class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
# 增加一个方法add绑定的函数是一个匿名函数,该函数执行的操作是往listappend一个元素value
attrs['add'] = lambda self,value:self.append(value)
return type.__new__(cls,name,bases,attrs)
匿名函数简化代码代码不容易理解,以下直接定义一个函数然后再绑定的方法更容易理解,和上面使用type函数添加方法的例子类似
def add(self,value):print(self)
self.append(value)
attrs['add'] = add
有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass
:
class MyList(list,metaclass=ListMetaclass):pass
当我们传入关键字参数metaclass
时,魔术就生效了,它指示Python解释器在创建MyList
时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()
方法接收到的参数依次是:
当前准备创建的类的对象;
类的名字;
类继承的父类集合;
类的方法集合。
测试一下MyList
是否可以调用add()
方法:
L = MyList()L.add(1)
print(L)
# [1]
而普通的list
没有add()
方法:
>>> L2 = list()>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'
动态修改有什么意义?直接在MyList
定义中写上add()
方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。
直接在MyList中添加add()方法代码如下
test.py
# 正常方法在MyList添加add方法 startclass MyList(list):
def add(self,value):
self.append(value)
# 实例化,相当于创建了一个空的list
L = MyList()
# 调用类的add方法,相当于执行了类的内部函数add(self,value)
# 传递的参数为self即本身这个空的list可以省略,value为1
# 执行add方法相当于执行了L.append(1)往list添加一个元素1
L.add(1)
# 空list使用了append方法添加一个元素1打印list为包含一个1个元素的list
print(L)
# [1]
# 常方法在MyList添加add方法 end
使用metaclass元类创建的新类到底执行了上面操作,下面我们通过打印__new__()函数的几个参数来分析
修改代码
# metaclass是类的模板,所以必须从type类型派生class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
print('参数cls为: %s' % cls)
print('参数name为: %s' % name)
print('参数bases为: %s' % bases)
# 增加add方法前打印attrs
print('参数attrs为: %s' % attrs)
# 以下添加方法和匿名函数效果一样
# def add(self,value):
# print(self)
# self.append(value)
# attrs['add'] = add
# 增加一个方法add绑定的函数是一个匿名函数,该函数执行的操作是往listappend一个元素value
attrs['add'] = lambda self,value:self.append(value)
# 增加add方法后打印attrs
print('增加add方法后参数attrs为: %s' % attrs)
return type.__new__(cls,name,bases,attrs)
class MyList(list,metaclass=ListMetaclass):
pass
L = MyList()
L.add(1)
print(L)
# [1]
执行输出如下
参数cls为: <class '__main__.ListMetaclass'>参数name为: MyList
参数bases为: <class 'list'>
参数attrs为: {'__module__': '__main__', '__qualname__': 'MyList'}
增加add方法后参数attrs为: {'__module__': '__main__', '__qualname__': 'MyList', 'add': <function ListMetaclass.__new__.<locals>.<lambda> at 0x0000016B0D9BE288>}
[1]
很明显函数__new__()对应的4个参数
# 当前准备创建类的对象即当前类使用那一个MetaClass类来创建,本次是使用ListMetaclass参数cls为: <class '__main__.ListMetaclass'>
# 当前创建的类的名字MyList即当前需要使用MetaCLass来创建的类,即在定义类时使用了关键字参数metaclass
参数name为: MyList
# 类继承的父类集合,本次继承的父类为list
参数bases为: <class 'list'>
# 类的方法集合,默认在没有任何修改时包含两个方法
参数attrs为: {'__module__': '__main__', '__qualname__': 'MyList'}
# 在__new__内部添加了一个新方法add任何把添加方法后的新类返回了
增加add方法后参数attrs为: {'__module__': '__main__', '__qualname__': 'MyList', 'add': <function ListMetaclass.__new__.<locals>.<lambda> at 0x0000016B0D9BE288>}
对应的关系图示如下
个人理解:感觉元类像一个装饰器,把一个类装饰以后再返回一个新的类。
下面通过调试模式看一遍执行过程
动态修改有什么意义?直接在MyList
定义中写上add()
方法不是更简单吗?正常情况下,确实应该直接写,通过metaclass修改纯属变态。
但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM全称“Object Relational Mapping”,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个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()
其中,父类Model
和属性类型StringField
、IntegerField
是由ORM框架提供的,剩下的魔术方法比如save()
全部由父类Model
自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。
现在,我们就按上面的接口来实现该ORM。
首先来定义Field
类,它负责保存数据库表的字段名和字段类型:
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)
在Field
的基础上,进一步定义各种类型的Field
,比如StringField
,IntegerField
等等:
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)
以及基类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))
当用户定义一个class User(Model)
时,Python解释器首先在当前类User
的定义中查找metaclass
,如果没有找到,就继续在父类Model
中查找metaclass
,找到了,就使用Model
中定义的metaclass
的ModelMetaclass
来创建User
类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass
中,一共做了几件事情:
排除掉对
Model
类的修改;在当前类(比如
User
)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个__mappings__
的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性);把表名保存到
__table__
中,这里简化为表名默认为类名。
在Model
类中,就可以定义各种操作数据库的方法,比如save()
,delete()
,find()
,update
等等。
我们实现了save()
方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT
语句。
编写代码试试:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')u.save()
输出如下
Found model: UserFound mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,id) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
可以看到,save()
方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。
可以看到执行save()方法输出了实现插入数据库的语句,只要建立了连接池执行以下类似语句
cur.execute(sql,args)
其中sql是执行的语句,在sql内可以使用%s进行类似格式化的替换,替换的内容为args列表内的元素
使用异步连接MySQL执行sql语句的用法可参考:https://www.cnblogs.com/minseo/p/15538636.html
不到100行代码,我们就通过metaclass实现了一个精简的ORM框架,是不是非常简单?
以上代码顺序有点乱,下面贴出全部代码
orm.py
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()
print(attrs)
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 # 假设表名和类名一致
print(attrs)
return type.__new__(cls, name, bases, attrs)
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 = []
print(self)
for k, v in self.__mappings__.items():
print(k,v)
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))
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)
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')
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()
下面拆分来分析执行过程
首先我们看一下整个类的关系拓扑图
其中StringField和IntergerField继承至类Field ,User继承类Model,Model类定义了元类方法metaclass,所以User类在创建的时候首先会在自己定义的参数查找metaclass没有找到,去父类找metaclass找到了,所以User类也会使用MoelMedaClass进行创建。
在整体分析执行过程之前我们来拆分解析
1,拆分解析类User的创建过程
test.py
看以下代码,为了方便解析,我们定义了字段类Field然后又分别定义了字符串类StringField和整数字段类IntergerField他们都继承了类Filed
然后我们定义了User的父类Model类,继承dict类,这里我们没有设置关键字参数metaclass,所以不会执行重新创建类的步骤
接下来我们定义了User类继承了Model,即相当于继承dict,如果在User类内部没有定义任何属性和方法则User类其实就是一个dict类
我们在类的内部定义了4个属性分别是id name email password他们对应的值则是实例化以后的一个实例
例如属性id对应的是一个通过IntegerField类实例化以后的实例
# 拆分解析类User startclass Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回实例对象的时候好看一点默认返回为 <__main__.StringField object at 0x0000025CC313EF08>
# 定义了__str__返回为 <StringField:email>
# 可以省略使用默认也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定义字符串类继承至Field
class StringField(Field):
def __init__(self,name):
# 继承父类的初始化方法
# Python3可以省略参数(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
# 拆分解析Model没有定义metaclass关键字参数,只是继承了类dict
class Model(dict):
pass
# 定义类User继承Model所以User继承了dict的方法和属性
class User(Model):
# 除了dict的方法和属性,User添加一下属性
# 该属性的key即为id,name,email,password对应的值则为实例化以后的实例
# 例如属性id对应的就是通过类IntegerField()传递参数'id'生成的实例
# 该实例继承至类Field,类Field在初始化的时候定义了两个属性name和column_type
# 分别代表的就是数据库表里对应字段名称和字段类型 本次字段名为'id',字段属性为'bigint'长整数
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 实例化,因为User继承了字典,所以可以以键值对的方式赋值
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 打印实例u,其实就是一个字典
print(u)
# {'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
# 但是这个字典除了字典有的所有方法以外还继承了类User定义的几个属性 id,name,emai,password
# 下面遍历打印出这几个属性,这几个属性是从类User继承的,所以如果把u修改为类User打印结果也是一样的
for i in dir(u):
if i in ['id','name','email','password']:
print("%s:%s" % (i,getattr(u,i,None)))
# email:<StringField:email>
# id:<IntegerField:id>
# name:<StringField:username>
# password:<StringField:password>
# 拆分解析类User end
输出如下
{'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}email:<StringField:email>
id:<IntegerField:id>
name:<StringField:username>
password:<StringField:password>
下面通过调试模式分析执行过程
省略重复的几个步骤,分别又往类User添加了属性name,email,password
遍历完所有实例u的属性,然后本次只打印了对应个新增的4个属性
补充:getattr使用方法
getattr(object,i,None)# 其中object是一个对象
# i为去对象中查找对应i属性,如果有对应属性则把属性值返回
# None 如果没有对应属性则返回None
修改代码我们可以打印对应实例分别对应的属性name和colume_tpye
for i in dir(u):if i in ['id','name','email','password']:
# print("%s:%s" % (i,getattr(u,i,None)))
print("%s:%s" % (getattr(u,i,None).name,getattr(u,i,None).column_type))
输出如下
email:varchar(100)id:bigint
username:varchar(100)
password:varchar(100)
2,拆分分析类User的内部属性
首先我们在定义的metacalss里面把类原样返回不进行任何修改,代码如下
test.py
# 拆分解析添加关键字参数metaclass但是不对类进行修改 startclass Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回实例对象的时候好看一点默认返回为 <__main__.StringField object at 0x0000025CC313EF08>
# 定义了__str__返回为 <StringField:email>
# 可以省略使用默认也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定义字符串类继承至Field
class StringField(Field):
def __init__(self,name):
# 继承父类的初始化方法
# Python3可以省略参数(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果类是Model则不做任何修改,类原样返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否则执行对类的重新定义,本次没有定义,还是原样返回
print('参数cls为: %s' % cls)
print('参数name为: %s' % name)
print('参数bases为: %s' % bases)
# 增加add方法前打印attrs
print('参数attrs为: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
pass
# 定义类User继承Model所以User继承了dict的方法和属性
class User(Model):
# 除了dict的方法和属性,User添加一下属性
# 该属性的key即为id,name,email,password对应的值则为实例化以后的实例
# 例如属性id对应的就是通过类IntegerField()传递参数'id'生成的实例
# 该实例继承至类Field,类Field在初始化的时候定义了两个属性name和column_type
# 分别代表的就是数据库表里对应字段名称和字段类型 本次字段名为'id',字段属性为'bigint'长整数
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 实例化,因为User继承了字典,所以可以以键值对的方式赋值
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 打印实例u,其实就是一个字典
print(u)
# {'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
# 但是这个字典除了字典有的所有方法以外还继承了类User定义的几个属性 id,name,emai,password
# 下面遍历打印出这几个属性,这几个属性是从类User继承的,所以如果把u修改为类User打印结果也是一样的
# getarrt方法从对象中获取对应属性,如果对象包含该属性则返回属性值,如果不包含对应属性则返回None
for i in dir(u):
if i in ['id','name','email','password']:
print("%s:%s" % (i,getattr(u,i,None)))
# print("%s:%s" % (getattr(u,i,None).name,getattr(u,i,None).column_type))
# email:<StringField:email>
# id:<IntegerField:id>
# name:<StringField:username>
# password:<StringField:password>
# 拆分解析添加关键字参数metaclass但是不对类进行修改 end
从代码我们可以看到我们给类Model添加了关键字参数metaclass=ModelMetaclass使用ModelMetaclass对类进行重新创建
我们不需要修改类Model只需要修改类User,以下代码排除对Model的修改
if name == 'Model':return type.__new__(cls, name, bases, attrs)
当创建到类User时,首先在类User里面找metaclass结果没有找到,就去它的父类查找有没有关键字参数metaclass找到了,所以使用定义的类ModelMetaclass对类User进行重新创建,但是本次代码我们只是打印了对应的几个参数并没有修改,相当于类还是原样返回了。
运行输出如下
参数cls为: <class '__main__.ModelMetaclass'>参数name为: User
参数bases为: <class '__main__.Model'>
参数attrs为: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x0000016D9A026BC8>, 'name': <__main__.StringField object at 0x0000016D9A026C08>, 'email': <__main__.StringField object at 0x0000016D9A026C48>, 'password': <__main__.StringField object at 0x0000016D9A026C88>}
{'id': 12345, 'name': 'Michael', 'email': 'test@orm.org', 'password': 'my-pwd'}
email:<StringField:email>
id:<IntegerField:id>
name:<StringField:username>
password:<StringField:password>
我们重点看输出的attrs除了固定的两个属性
'__module__': '__main__', '__qualname__': 'User'
多出的几个属性就是在类User里面定义的4个属性,
'id': <__main__.IntegerField object at 0x0000016D9A026BC8>, 'name': <__main__.StringField object at 0x0000016D9A026C08>, 'email': <__main__.StringField object at 0x0000016D9A026C48>, 'password': <__main__.StringField object at 0x0000016D9A026C88>
3,拆分使用metaclass对类User重新创建
通过上面的例子在ModelMetaclass内没有对类User进行修改重新创建,只是输出了类User的__attr__属性,我们可以看到除了默认的两个属性以为,类User多出来的几个属性分别为id,name,email,password他们对应的值为实例化以后的一个实例。
下面我们来分析一个MySQL的插入语句,看一下我们需要哪些参数,以及我们是否可以通过查找User对应的__attr__找到这些参数
假如我们要往表里插入一条数据,应该使用以下语句
insert into user (id,username,emai,password) values (12345,'Michael','test@orm.org','my-pwd')
插入语句的格式为
insert into # 固定格式user # 表名
(id,username,emai,password) # 字段名
values # 固定格式
(12345,'Michael','test@orm.org','my-pwd') # 字段的值
除了固定格式的insert into和values,我们需要从User对应的__attr__中提取的内容有表名,字段名,字段的值
其中表名我们可以定义为和类名一样本次为user,可以通过函数__new__(cls, name, bases, attrs)的参数name获取到name=‘User’
字段名(id,username,emai,password)的获取,例如我想要获得字段id则可以通过实例u的id属性获得一个StringField实例,然后再通过这个实例的name属性获得对应的字段名
通过实例打印对应的4个字段名
print(u.id.name,u.name.name,u.email.name,u.password.name)# id username email password
解析:u.id为对应的实例IntegerField('id'),然后再使用属性name则可以获得字段名。
字段的值(12345,'Michael','test@orm.org','my-pwd') 我们可以直接从实例化后的字典中通过key获取
print(u['id'],u['name'],u['email'],u['password'])# 12345 Michael test@orm.org my-pwd
但是通过key获取只能是使用字典的dict[key]方式来获取,如果想要通过属性的方式获取例如u.id或getattr(u,'id',None)获取到的是类的id属性即IntegerField('id')实例
示例如下
print(u.id,u.name,u.email,u.password)print(getattr(u,'id',None),getattr(u,'name',None),getattr(u,'email',None),getattr(u,'password',None))
上面两种方式取获取实例u的id属性效果的一样的,获取到的都是对应的实例,而不是我们想要的字段值12345 Michael test@orm.org my-pwd
<IntegerField:id> <StringField:username> <StringField:email> <StringField:password><IntegerField:id> <StringField:username> <StringField:email> <StringField:password>
补充:字典实例想要通过key去获取该可以对应的值有两种方式
①使用dict[key]方法获取,这个是字典自带的最常用的方法
②使用dict.key属性方式获取,需要在类里面定义__getattr__(),如果没有定义__getattr__方法则字典类型的实例是无法通过属性去获取值的
示例如下
class Dict(dict):# 需要定义__getattr__方法才能通过属性获得值
def __getattr__(self,key):
return self[key]
d = Dict(id=456,name='李四')
print(d.id)
输出为
456
假如类定义了相同的属性则使用属性获取会获得类的属性
class Dict(dict):id = 123
# 需要定义__getattr__方法才能通过属性获得值
def __getattr__(self,key):
return self[key]
d = Dict(id=456,name='李四')
print(d.id)
输出为得到的是类的属性值不是示例的属性值
123
需要得到实例的对应属性值只能是通过字典的方法
print(d['id'])
假如我们现在想要通过实例的属性来获取实例的值,即我想通过u.id获取的值和u['id']的到的值是一样的,应该怎么办?
我们可以把原始的__attr__里面对应的4个属性id,name,email,password提取出来组成一个字典,然后把这个字典作为一个value,重新定义一个key为'__mappings__'用于存储这个字典组成的value,然后再把原__attr__里面的这4个属性删除掉,这样实例u的属性和类User的属性就不会冲突了,定义好__getattr__方法就可以使用属性的方法去获得字段对应的值。
修改后的代码如下
# 使用metaclass对类进行修改 startclass Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回实例对象的时候好看一点默认返回为 <__main__.StringField object at 0x0000025CC313EF08>
# 定义了__str__返回为 <StringField:email>
# 可以省略使用默认也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定义字符串类继承至Field
class StringField(Field):
def __init__(self,name):
# 继承父类的初始化方法
# Python3可以省略参数(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果类是Model则不做任何修改,类原样返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否则执行对类的重新定义,本次没有定义,还是原样返回
print('参数cls为: %s' % cls)
print('参数name为: %s' % name)
print('参数bases为: %s' % bases)
# 增加add方法前打印attrs
print('参数attrs为: %s' % attrs)
# 定义一个空字典,用于存储对应的属性和值
mappings = {}
# 遍历attr字典,如果对应的v是类Field的子集则安装key的方法存储到字典中
for k,v in attrs.items():
if isinstance(v,Field):
mappings[k] = v
# 原attrs删除对应的属性
for k in mappings:
attrs.pop(k)
attrs['__mappings__'] = mappings
attrs['__tabel__'] = name
print('修改后参数attrs为: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
pass
# 定义类User继承Model所以User继承了dict的方法和属性
class User(Model):
# 除了dict的方法和属性,User添加一下属性
# 该属性的key即为id,name,email,password对应的值则为实例化以后的实例
# 例如属性id对应的就是通过类IntegerField()传递参数'id'生成的实例
# 该实例继承至类Field,类Field在初始化的时候定义了两个属性name和column_type
# 分别代表的就是数据库表里对应字段名称和字段类型 本次字段名为'id',字段属性为'bigint'长整数
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 使用metaclass对类进行修改 end
运行输出如下
参数cls为: <class '__main__.ModelMetaclass'>参数name为: User
参数bases为: <class '__main__.Model'>
参数attrs为: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x000002C02B419B48>, 'name': <__main__.StringField object at 0x000002C02B419B88>, 'email': <__main__.StringField object at 0x000002C02B419BC8>, 'password': <__main__.StringField object at 0x000002C02B419C08>}
修改后参数attrs为: {'__module__': '__main__', '__qualname__': 'User', '__mappings__': {'id': <__main__.IntegerField object at 0x000002C02B419B48>, 'name': <__main__.StringField object at 0x000002C02B419B88>, 'email': <__main__.StringField object at 0x000002C02B419BC8>, 'password': <__main__.StringField object at 0x000002C02B419C08>}, '__tabel__': 'User'}
我们对比修改前的attrs和修改后的attrs有什么不同
①把User对应的4个属性重新放到一个字典中,并且创建对应的key为'__mappings__'
②使用pop方法删除了原有的4个属性,如果不删除则还是会冲突
③往attrs添加一个字段'__table__'用于存储表名,这里我们假设数据库的表名就是类名,当前创建的类名可以通过参数name获取到
4,在父类Model中创建save()方法
使用metaclass对类User的修改已经完成下面我们在User的父类创建一个save()方法用于执行MySQL的insert语句即插入语句
我们知道使用MySQL连接池创建浮标然后执行sql语句的格式为
cur.execute(sql, args)
其中sql为需要执行的sql语句,args为带的参数,例如我们要往数据库的表user中插入一条数据执行方式为
sql = 'insert into user (id,username,email) values(%s,%s,%s,%s)'args = [12345,'Michael','test@orm.org','my-pwd']
cur.excute(sql,args)
下面我们在save()方法中去获取对应的参数,代码如下
# 在类Model中定义save()方法 startclass Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type = column_type
# 返回实例对象的时候好看一点默认返回为 <__main__.StringField object at 0x0000025CC313EF08>
# 定义了__str__返回为 <StringField:email>
# 可以省略使用默认也可以
def __str__(self):
return '<%s:%s>' %(self.__class__.__name__,self.name)
# 定义字符串类继承至Field
class StringField(Field):
def __init__(self,name):
# 继承父类的初始化方法
# Python3可以省略参数(StringField,self)
# super(StringField,self).__init__(name,'varchar(100)')
super().__init__(name,'varchar(100)')
class IntegerField(Field):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
# 如果类是Model则不做任何修改,类原样返回
if name == 'Model':
return type.__new__(cls, name, bases, attrs)
# 否则执行对类的重新定义,本次没有定义,还是原样返回
print('参数cls为: %s' % cls)
print('参数name为: %s' % name)
print('参数bases为: %s' % bases)
# 增加add方法前打印attrs
print('参数attrs为: %s' % attrs)
# 定义一个空字典,用于存储对应的属性和值
mappings = {}
# 遍历attr字典,如果对应的v是类Field的子集则安装key的方法存储到字典中
for k,v in attrs.items():
if isinstance(v,Field):
mappings[k] = v
# 原attrs删除对应的属性
for k in mappings:
attrs.pop(k)
attrs['__mappings__'] = mappings
attrs['__tabel__'] = name
print('修改后参数attrs为: %s' % attrs)
return type.__new__(cls, name, bases, attrs)
class Model(dict,metaclass=ModelMetaclass):
# 定义__getattr__方法,改方法传递一个key值然后使用字典的取值方式返回
# 不定的这个方法无法使用属性的方式获取值
def __getattr__(self,key):
return self[key]
def save(self):
# 定义空list用于存储字段名称
fields = []
# 定义空list用于存储占位符'?'
params = []
# 定义空list用于存储字段的值
args = []
# 使用k,v的方式遍历k为对应的属性如id v为对应的实例
for k,v in self.__mappings__.items():
# print(k,v)
# 通过实例的属性获取字段名
fields.append(v.name)
# 通过属性从实例获取到对应字段的值
# 需要定义__getattr__方法,这里不能使用self.k这种方法来获取,因为使用这种方法k是作为一个属性值而不是变量
args.append(getattr(self,k,None))
# 每增加一个字段则增加一个占位符?
params.append('?')
print(fields)
print(args)
# 使用join方法把list拼接成str
sql = 'insert into %s (%s) values (%s)' %(self.__tabel__,','.join(fields),','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
# 定义类User继承Model所以User继承了dict的方法和属性
class User(Model):
# 除了dict的方法和属性,User添加一下属性
# 该属性的key即为id,name,email,password对应的值则为实例化以后的实例
# 例如属性id对应的就是通过类IntegerField()传递参数'id'生成的实例
# 该实例继承至类Field,类Field在初始化的时候定义了两个属性name和column_type
# 分别代表的就是数据库表里对应字段名称和字段类型 本次字段名为'id',字段属性为'bigint'长整数
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')
# 执行save()方法,本次只是模拟执行MySQL的语句,并没有真正执行MySQL语句
u.save()
# 在类Model中定义save()方法 end
执行输出如下
参数cls为: <class '__main__.ModelMetaclass'>参数name为: User
参数bases为: <class '__main__.Model'>
参数attrs为: {'__module__': '__main__', '__qualname__': 'User', 'id': <__main__.IntegerField object at 0x0000022A0DCCDFC8>, 'name': <__main__.StringField object at 0x0000022A0DCD4048>, 'email': <__main__.StringField object at 0x0000022A0DCD4088>, 'password': <__main__.StringField object at 0x0000022A0DCD40C8>}
修改后参数attrs为: {'__module__': '__main__', '__qualname__': 'User', '__mappings__': {'id': <__main__.IntegerField object at 0x0000022A0DCCDFC8>, 'name': <__main__.StringField object at 0x0000022A0DCD4048>, 'email': <__main__.StringField object at 0x0000022A0DCD4088>, 'password': <__main__.StringField object at 0x0000022A0DCD40C8>}, '__tabel__': 'User'}
['id', 'username', 'email', 'password']
[12345, 'Michael', 'test@orm.org', 'my-pwd']
SQL: insert into User (id,username,email,password) values (?,?,?,?)
ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd']
我们可以看到执行save()方法把我们所需要的参数都获取到了,实际如果连接了数据库则可以执行相应的插入操作,同理通过定义select,update,delete方法可以执行其他针对数据库的操作。
以上是 Python之使用元类MetaClass 的全部内容, 来源链接: utcz.com/z/387231.html