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
1 2 3 4 5 | # 定义Hello类 class Hello( object ): # 定义类函数,该函数传递一个参数name然后打印,name设置默认值 def hello( self ,name = 'world' ): print ( 'Hello,%s.' % name) |
当Python解释器载入hello
模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello
的class对象,测试如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 测试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)...的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # 通过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
1 2 3 4 5 6 | # 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函数添加方法的例子类似
1 2 3 4 | def add( self ,value): print ( self ) self .append(value) attrs[ 'add' ] = add |
有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass
:
1 2 | class MyList( list ,metaclass = ListMetaclass): pass |
当我们传入关键字参数metaclass
时,魔术就生效了,它指示Python解释器在创建MyList
时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()
方法接收到的参数依次是:
-
当前准备创建的类的对象;
-
类的名字;
-
类继承的父类集合;
-
类的方法集合。
测试一下MyList
是否可以调用add()
方法:
1 2 3 4 | L = MyList() L.add( 1 ) print (L) # [1] |
而普通的list
没有add()
方法:
1 2 3 4 5 | >>> 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 正常方法在MyList添加add方法 start class 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__()函数的几个参数来分析
修改代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # 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] |
执行输出如下
1 2 3 4 5 6 | 参数 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个参数
1 2 3 4 5 6 7 8 9 10 | # 当前准备创建类的对象即当前类使用那一个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
,我们期待他写出这样的代码:
1 2 3 4 5 6 7 8 9 10 11 | 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
类,它负责保存数据库表的字段名和字段类型:
1 2 3 4 5 6 7 8 | 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
等等:
1 2 3 4 5 6 7 8 9 | 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
了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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
语句。
编写代码试试:
1 2 | u = User( id = 12345 , name = 'Michael' , email = 'test@orm.org' , password = 'my-pwd' ) u.save() |
输出如下
1 2 3 4 5 6 7 | Found model: User Found 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()方法输出了实现插入数据库的语句,只要建立了连接池执行以下类似语句
1 | cur.execute(sql,args) |
其中sql是执行的语句,在sql内可以使用%s进行类似格式化的替换,替换的内容为args列表内的元素
使用异步连接MySQL执行sql语句的用法可参考:https://www.cnblogs.com/minseo/p/15538636.html
不到100行代码,我们就通过metaclass实现了一个精简的ORM框架,是不是非常简单?
以上代码顺序有点乱,下面贴出全部代码
orm.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | 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类实例化以后的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | # 拆分解析类User start class 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 |
输出如下
1 2 3 4 5 | { '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使用方法
1 2 3 4 | getattr ( object ,i, None ) # 其中object是一个对象 # i为去对象中查找对应i属性,如果有对应属性则把属性值返回 # None 如果没有对应属性则返回None |
修改代码我们可以打印对应实例分别对应的属性name和colume_tpye
1 2 3 4 | 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)) |
输出如下
1 2 3 4 | email:varchar( 100 ) id :bigint username:varchar( 100 ) password:varchar( 100 ) |
2,拆分分析类User的内部属性
首先我们在定义的metacalss里面把类原样返回不进行任何修改,代码如下
test.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | # 拆分解析添加关键字参数metaclass但是不对类进行修改 start class 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的修改
1 2 | if name = = 'Model' : return type .__new__( cls , name, bases, attrs) |
当创建到类User时,首先在类User里面找metaclass结果没有找到,就去它的父类查找有没有关键字参数metaclass找到了,所以使用定义的类ModelMetaclass对类User进行重新创建,但是本次代码我们只是打印了对应的几个参数并没有修改,相当于类还是原样返回了。
运行输出如下
1 2 3 4 5 6 7 8 9 | 参数 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除了固定的两个属性
1 | '__module__' : '__main__' , '__qualname__' : 'User' |
多出的几个属性就是在类User里面定义的4个属性,
1 | '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__找到这些参数
假如我们要往表里插入一条数据,应该使用以下语句
1 | insert into user ( id ,username,emai,password) values ( 12345 , 'Michael' , 'test@orm.org' , 'my-pwd' ) |
插入语句的格式为
1 2 3 4 5 | 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个字段名
1 2 | 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获取
1 2 | 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')实例
示例如下
1 2 | 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
1 2 | <IntegerField: id > <StringField:username> <StringField:email> <StringField:password> <IntegerField: id > <StringField:username> <StringField:email> <StringField:password> |
补充:字典实例想要通过key去获取该可以对应的值有两种方式
①使用dict[key]方法获取,这个是字典自带的最常用的方法
②使用dict.key属性方式获取,需要在类里面定义__getattr__(),如果没有定义__getattr__方法则字典类型的实例是无法通过属性去获取值的
示例如下
1 2 3 4 5 6 | class Dict ( dict ): # 需要定义__getattr__方法才能通过属性获得值 def __getattr__( self ,key): return self [key] d = Dict ( id = 456 ,name = '李四' ) print (d. id ) |
输出为
1 | 456 |
假如类定义了相同的属性则使用属性获取会获得类的属性
1 2 3 4 5 6 7 | class Dict ( dict ): id = 123 # 需要定义__getattr__方法才能通过属性获得值 def __getattr__( self ,key): return self [key] d = Dict ( id = 456 ,name = '李四' ) print (d. id ) |
输出为得到的是类的属性值不是示例的属性值
1 | 123 |
需要得到实例的对应属性值只能是通过字典的方法
1 | print (d[ 'id' ]) |
假如我们现在想要通过实例的属性来获取实例的值,即我想通过u.id获取的值和u['id']的到的值是一样的,应该怎么办?
我们可以把原始的__attr__里面对应的4个属性id,name,email,password提取出来组成一个字典,然后把这个字典作为一个value,重新定义一个key为'__mappings__'用于存储这个字典组成的value,然后再把原__attr__里面的这4个属性删除掉,这样实例u的属性和类User的属性就不会冲突了,定义好__getattr__方法就可以使用属性的方法去获得字段对应的值。
修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | # 使用metaclass对类进行修改 start class 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 |
运行输出如下
1 2 3 4 5 | 参数 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语句的格式为
1 | cur.execute(sql, args) |
其中sql为需要执行的sql语句,args为带的参数,例如我们要往数据库的表user中插入一条数据执行方式为
1 2 3 | 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()方法中去获取对应的参数,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | # 在类Model中定义save()方法 start class 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 |
执行输出如下
1 2 3 4 5 6 7 8 9 | 参数 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方法可以执行其他针对数据库的操作。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
2020-11-18 Web开发基础之JavaScript
2020-11-18 Web开发基础之CSS