零:说明
Records是一个对python中关系型数据库查询的封装,简化了在python中进行数据库查询的操作。项目地址如下:GitHub – kennethreitz/records: SQL for Humans™
作者Kennethreitz是requests和python-guide 额作者,python领域的大牛人物之一,关于他还有一个励志的故事:谁说程序员不是潜力股?让这位世界前五名的天才程序员来颠覆你三观!
项目本身代码短小,总共只有一个文件不超过500行,如果是第一次尝试阅读python开源项目,这是一个很好的选择。正好最近在学习数据库方面的相关知识,也可以加深一下对关系型数据库的理解。
Records实现了对直接输入SQL语句(raw SQL)进行查询的返回结果的优化,并没有实现ORM之类高级的技巧,这种注重使用体验的优化,正如其名:SQL for humans。尽管如此,作者所使用的架构技巧,包括惰性查询,缓存的使用,还是有着一定的借鉴意义。
一:项目分析
这个项目中的代码并不是很多,因此打算将项目里的每个类都分析一下。虽然逻辑并不复杂,但是作者对每个类的设计和架构,对python中类相关技巧都使用,都有一些可以学习的地方。
Record
这是用来存储查询结果(也就是数据库中的一行)的类,用了很多类的特殊方法进行架构,因此重点分析这些特殊方法。
python提供特殊命名的方法来拦截运算,以双下划线(__X__
)命名的钩子与内置运算一一对应。
__slots__
__slots__
包含类属性的字符串,可以限制类的合法属性集,只对新式类有作用。是为了节省实例的内存消耗,类中的方法不受其限制。新式类中原本含有一个__dict__
属性,用来记录实例中所有的属性和方法,也是通过这个字典,可以让实例绑定任意的属性,而__slots__
存在时,就不会有__dict__
变量。
__slots__
定义的属性仅对当前类起作用,对继承的子类是不起作用的,除非在子类中也定义__slots__
,这样,子类允许定义的属性就是自身的__slots__
加上父类的__slots__
。__getitem__()
__getitem__()
是对[]
的重载,同时,函数内使用了index()
对列表中的内容反查其索引,实现了通过record[key]
查找value
的功能。如果是我大概会用一个索引迭代,但实际上不需要。
__dir__()
__dir__()
方法指明了对实例使用内置函数dir()
时需要返回的列表, 源代码中在基类的基础上加上了keys
的值列表。其他
对于类实例的显示用了另一个他自己写的库
tablib
,用于对数据进行格式化。在as_dict()中用到了一个三目运算符
A = X if conditon else Y #等价于: if conditon: A = X else:_all_rows A = Y
RecordCollection
这个类是Record的集合类,用于存储查询(query)结果。
构造
构造函数传入一个逐行的
Record generator
,存储在成员变量_rows
中。generator
支持迭代,通过圆括号形成的生成器表达式生成,例如:(x for x in range(5)) # <generator object <genexpr> at 0x7fd55f3fe9b0>
__iter__()
&yield
&next()
__iter__()
和next()
与迭代器有关,在Python中实现了__iter__()
方法的对象是可迭代的,实现了next()
方法的对象是迭代器(iterator)。
以for
循环为例,事实上,for i in obj
的迭代就是通过内置函数iter(obj)
调用它的__iter__()
方法取得一个迭代器对象(但在源码中是返回了一个生成器,见下文说明)。源码中在
__iter__()
函数内部使用了yeild
关键字。含有yield
的函数称为生成器函数,生成器函数返回时使用yield
关键字,与return
的用法类似,但是,生成器函数返回时会保存包含其本地作用域的状态,之后将函数挂起,在下次调用函数时,从挂起的地方继续。事实上,含有yield
的语句被调用时会产生一个生成器(generator)从而支持迭代。生成器和迭代器都通过
next()
方法不断生成下一个对象,如果迭代到达末尾,在next()
中会抛出StopIteration
异常。<u>generator自身是一个iterator,但只支持一次迭代。</u>
缓存
_all_rows
变量用于缓存,缓存有几种不同的情况需要考虑:- 顺序迭代,在
next()
中一旦执行了_rows_.next()
就缓存迭代结果。 - 直接调用
collection[index]
操作获取指定行,在__getitem__()
中会顺序迭代到指定位置,对在指定行之前的行全部缓存,同时,之后如果再调用iterator,就不需要调用iterator.next()
而是直接从缓存中取用,这一点在__iter__()
中也有体现。 - 要显示对象本身的时候,会显示其中包含的行,此时要求对
_rows
迭代求值,并且缓存。在源代码中,位于all()
函数中,通过rows = list(self)
调用类的__iter__()
方法。
- 顺序迭代,在
Database
这是执行数据库查询与数据库连接的类。
SQLAlchemy
数据库查询主要依赖于SQLAlchemy库的调用,这是一个python的ORM框架。ORM即对象关系映射,简而言之,就是把数据库的一个个
table
(表),映射为编程语言的class
(类),这里主要介绍在源码中使用到的几个方法。SQLAlchemy实际上可以将数据库查询完全转化为对象的调用,但是在records这个库里,只是对原生的数据库查询返回进行了封装,所有并没有使用到这些功能。详细使用方法:SQLAlchemy 1.1 Documentation
create_engine()
方法进行数据库连接,返回一个database对象。之后通过engine.connect()
获取connection
, 然后通过其执行sql, 叫做connection执行,其实通过engine.execute()
也可以执行sql,不过connection执行与使用transaction模式有关, 如果不涉及transaction, 两种方法效果是一样的。inspect()
方法创建了一个Inspector
对象。通过这个对象,可以实现对数据库中的schema
,table
,constraint
等结构的查询。例如,insp(engine).get_table_names()
。connection.excute("sql")
返回一个p = ResultProxy
对象,通过p.keys()
可以得到查询的列名,而通过迭代p
可以获得查询的每一行,以此建立一个RecordCollection
。text()
用于提供附带参数的查询功能,例如:t = text("SELECT * FROM users WHERE id=:user_id") result = connection.execute(t, user_id=12)
上下文管理器
有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python提供了一些解决的方案(以文件读写为例)。
利用
try-finally
语句块来进行包装,这种方法易于理解,但是当逻辑复杂时就会十分混乱:writer = open(filename, mode) try: writer.write(“”) finally: writer.close()
通过
with
语法重写以上逻辑,就可以简化为:with open(filename, mode) as writer: writer.write(“”)
事实上,
with
语句基本思想是with
所求值的对象必须有一个__enter__()
方法,一个__exit__()
方法。紧跟with
后面的语句被求值后,返回对象的__enter__()
方法被调用,这个方法的返回值将被赋值给as
后面的变量。当with后面的代码块全部被执行完之后,将调用前面返回对象的__exit__()
方法。__exit__()
还可以处理异常,在with
后面的代码块抛出任何异常时,__exit__()
方法被执行。异常抛出时,与之关联的type,value和stack trace传给__exit__(self, exc_type, exc_val, exc_stack)
方法。因此,可以在清理资源,关闭文件等等操作后,将异常重新抛出。
query_file
通过
open()
操作读取一个文件,然后通过query()
函数执行内部sql语句。transition
通过
connection.begin()
方法实现。