谈谈Python中元类Metaclass(二):ORM实践
什么是ORM?
ORM的英文全称是“Object Relational Mapping”,即对象-关系映射,从字面上直接理解,就是把“关系”给“对象”化。
对应到数据库,我们知道关系数据库(例如Mysql)的特征就是数据与数据之间存在各种各样的“关系”,这种“关系”是由Table(表)来维护和表现的。
ORM就是把关系数据库的一个"表"映射成一个"类",然后给"类"添加各种各样的方法(比如增删改查)。
也就是说,我们要完成这样的设计:user可以根据实际需求(表的定义,比如包括哪些数据字段等)把通过ORM把"表"(table)设计成一个"类"(class),这就需要用到Metaclass的特性: 动态的创建类(class)
这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为我们知道,在数据库使用过程中,Mysql的“表”是由User来创建的;对应过来,那么“表”对应的类也是应该由User来创建的。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。
先搞清楚user会如何使用这个ORM框架:使用这个ORM框架,创建一个Customer类来操作对应的数据库表customer
,我们期待他写出这样的代码:
- class Customer(Model):
- # 定义类的属性到列的映射:
- id = IntegerField('id')
- name = StringField('username')
- email = StringField('email')
- password = StringField('password')
- # 创建一个实例:
- u = Customer(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
- # 保存到数据库:
- u.save()
其中,父类Model
和属性类型StringField
、IntegerField
是由ORM框架提供的。显然,save()等方法应该由ORM提供,这样既安全又使得user不用重复定义这些方法。具体实现上,save()等
全部由metaclass和基类Model自动完成。虽然metaclass和基类Model的编写会比较复杂,但ORM的使用者用起来却异常简单。
现在,我们就按上面的接口来实现该ORM。
首先搞清楚我们应该做的工作:
- 1. 既然要使得user能够通过继承Model,实现动态的创建类,那么必然要在ModelMetaclass中实现动态类的创建(比如能实现类Cutomer中的定义的attr属性们)
- 2. 在基类Model中实现save等方法。
首先来定义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)
当用户定义一个class Customer(Model)
时,Python解释器首先在当前类Customer的定义中查找metaclass
,如果没有找到,就继续在父类Model
中查找metaclass
,找到了,就使用Model
中定义的metaclass
的ModelMetaclass
来创建Customer类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass
中,一共做了几件事情:
排除掉对
Model
类的修改(肯定要避免user通过调用ModelMetaclass来修改我们定义好的基类Model);在当前类(比如Customer)中查找user定义的类(也就是例子中的Customer)的所有属性,如果找到一个Field属性,就把它保存到一个
__mappings__
的dict中,同时从类(Customer)的属性中删除该Field属性,否则,容易造成运行时错误(实例的属性会遮盖类的同名属性,也就是说保证Customer这个类里没有id,name等属性,从而不会影响Customer的实例的值; 这里之所以为了避免出现实例与类的同名属性,既避免运行时错误,是因为当出现类和实例的同名属性时,python会优先调用实例属性,如果没有的话则会调用类的属性,但是有时当实例属性不存在时,我们并不希望用户能调用到这个同名的类属性,比如当实例属性被删除时,再用相同的名字,就访问到了类属性了。因此才这么做);把表名保存到
__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
类中,就可以定义各种操作数据库的方法,比如save()
,delete()
,find()
,update
等等。
我们实现了save()
方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT
语句。
编写代码试试:
- u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
- u.save()
输出如下:
- 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语句,就可以完成真正的功能。
最终,我们实现了关系型数据库的ORM框架,user可以通过继承Model类,把各种各样的表写成对象,进行操作。
总结一下,通过ORM的这个例子,我们可以看出Metaclass的作用总结起来就三点:
1. 拦截类的创建
2. 修改类
3. 返回修改之后的类
参考链接:
深刻理解Python中的元类(metaclass)
谈谈Python中元类Metaclass(二):ORM实践的更多相关文章
- 谈谈Python中元类Metaclass(一):什么是元类
简单的讲,元类创建了Python中所有的对象. 我们说Python是一种动态语言,而动态语言和静态语言最大的不同,就是函数和类不是编译时定义的,而是运行时动态创建的. 比方说我们要定义一个HelloW ...
- python中元类(metaclass)的理解
原文地址:http://www.cnblogs.com/tkqasn/p/6524879.html 一:类也是对象 类就是一组用来描述如何生成一个对象的代码. 类也是一个对象,只要你使用关键字clas ...
- python中的类(二)
python中的类(二) 六.类的成员 字段:普通字段,静态字段 eg: class Province(): country=’中国’ #静态字段,保存在类中,执行时可以通过类或对象访问 def __ ...
- Python中元类
元类(metaclass) 简单地说,元类就是一个能创建类的类,而类class 是由type创建的,class可以创建对象 type与object的关系详见:python中type和object 1. ...
- 对python中元类的理解
1. 类也是对象 在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段.在Python中这一点仍然成立: >>> class ObjectCreator(object): ...
- python基础----元类metaclass
1 引子 class Foo: pass f1=Foo() #f1是通过Foo类实例化的对象 python中一切皆是对象,类本身也是一个对象,当使用关键字class的时候,python解释器在加载cl ...
- python 通过元类控制类的创建
一.python中如何创建类? 1. 直接定义类 class A: a = 'a' 2. 通过type对象创建 在python中一切都是对象 在上面这张图中,A是我们平常在python中写的类,它可以 ...
- [转]深刻理解Python中的元类(metaclass)以及元类实现单例模式
使用元类 深刻理解Python中的元类(metaclass)以及元类实现单例模式 在看一些框架源代码的过程中碰到很多元类的实例,看起来很吃力很晦涩:在看python cookbook中关于元类创建单例 ...
- 深刻理解Python中的元类(metaclass)以及元类实现单例模式
在理解元类之前,你需要先掌握Python中的类.Python中类的概念借鉴于Smalltalk,这显得有些奇特.在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段.在Python中这一点仍 ...
随机推荐
- Codeforces Round #327 (Div. 2) B Rebranding(映射)
O(1)变换映射,最后一次性替换. #include<bits/stdc++.h> using namespace std; typedef long long ll; ; char s[ ...
- 待解决问题:c++栈对象的析构、虚拟内存与内存管理的关系、内存管理的解决方案。
待解决问题:c++栈对象的析构.虚拟内存与内存管理的关系.内存管理的解决方案.
- 【洛谷5113】Sabbat of the witch(毒瘤分块)
点此看题面 大致题意: 给你一个序列,要你支持三种操作:区间赋值,区间求和,撤回之前任一区间赋值操作. 分块 这道题应该是一道十分毒瘤的分块题. 这道题要用到的算法并不是很难,但是思维难度是真的高. ...
- ElasticSearch High Level REST API【3】Scroll 滚屏
ES中提供了 FROM/SIZE 分页,但这种分页有性能瓶颈. Scroll会以间隔时间滚屏的方式返回全部的查询数据,可以作为数据量很大的情况下,分页的一个替代方案 完整的示例如下: public v ...
- git提交时报错 permission denied
git push 时报错:permission denied xxx 目前很多解决办法是生成公钥和秘钥,这种方法安全可靠,比较适用于一台电脑对应一个git账户,但是多个账户在同一台电脑上提交使用git ...
- 【Django】Django开发中的日志输出
开发环境:Ubuntu16.04+Django 1.11.9+Python2.7 一:使用自定义函数输出日志到log文件: import time def print_log(log): file_o ...
- linux下/dev/null被误删
/dev/null文件是一个特殊的设备文件,可以用于清空一些日志文件,或者是使一些信息输出到此文件,用以节省硬盘空间.如果该空文件/dev/null文件被误删除掉, 如何再使用系统命令重新创建并设置该 ...
- Linux-WebServer安装和配置
Apache 基本操作 解释 命令 安装 yum install httpd 启动 service httpd start 停止 service httpd stop 启动完成后 查看进程是否存在:p ...
- nodeJS 服务端文件上传
var http = require('http'); var path = require('path'); var fs = require('fs'); function uploadFile( ...
- Table 分页处理
介绍两种table分页处理:PHP分页 和 js(jquery.table)分页. 一:jquery.table: 1:下载两个文件:table_jui.css 和 jquery.dataTables ...