引言
本节笔记试图记录一些些在 Python 中使用数据库的方法以及其它数据持久化的技巧。总的来说,本节笔记的针对性强,应该不至于像之前的笔记那样,知识点凌乱不堪。
数据和持久化
- 一些持久化方案:
- 平面文件(Flat files):直接保存文本和字节;
- DBM键文件(DBM Keyed Files):按关键字访问存储在字典类文件中的字符串;
- Pickle 对象(Pickled objects):序列化 Python 对象到文件和流中;
- Shelve 文件(Shelve files):在DBM键文件中保存 Python pickle 对象;
- 面向对象的数据库(OODBs):保存 Python 对象到持久化字典中(比如ZODB或Durus);
- 关系型数据库(RDBMSs):基于表存储数据,支持SQL查询;
- 对象关系映射(ORMs):映射Python类型到关系型数据库中的表。
DBM 文件
- DBM 文件是 Python 库中数据库管理的标准工具之一,实现了数据的随机访问,可以通过键来访问存储的文本字符串。它是 Python 中存储信息最简单的方式之一。
- 看看如何使用 DBM 文件:
>>> import dbm
>>> file = dbm.open('conf', 'c')
>>> file['user'] = 'user name'
>>> file['password'] = 'password'
>>> file['user']
b'user name'
>>> file.keys()
[b'password', b'user']
>>> len(file)
2
>>> file['website'] = 'http://blog.chriscabin.com'
>>> file['foo'] = 'bar'
>>> file.keys()
[b'website', b'password', b'user', b'foo']
>>> del file['foo']
>>> file.keys()
[b'website', b'password', b'user']
>>> file.close()
>>> file = dbm.open('conf', 'c)
>>> file.keys()
[b'website', b'password', b'user']
>>> for key in file.keys():
print('{} => {}'.format(key, file[key]))
b'website' => b'http://blog.chriscabin.com'
b'password' => b'password'
b'user' => b'user name'
- DBM模块实际上是一个接口规范,并不去管系统具体使用的是什么DBM实现。
- 当打开一个存在的 DBM 文件时,dbm模块会使用
dbm.whichdb
函数检测文件使用的是哪种实现创建的,这种检测是基于文件的内容进行的。 - 当创建一个新文件是时,dbm模块使用固定的顺序来检测系统中存在的基于键的文件接口模块,它会按照
dbm.bsd
,dbm.gnu
,dbm.ndbm
,dbm.dumb
的顺序来尝试查找这些接口。如果都不存在,则使用最后一个 Python 自带的实现dbm.dumb
,当然性能和健全性都不如其它实现。
- 当打开一个存在的 DBM 文件时,dbm模块会使用
Pickle 对象
pickle
模块是一种超级通用的数据格式化和去格式化工具,几乎能把内存中任意的 Python 对象转换成字符串,以便存储在无格式的文件中,或者通过网络传输。这种行为叫做序列化。- 当一个对象从字节流重建(反序列化)时,它便成为一个和原对象一模一样的对象,拥有相同的数据结构和数据,但是位于不同于原来的内存位置。
- 由于在 Python 3.x 中,pickle 对象总是 bytes 类型的,而不是 str 类型,所以用来存储 pickle 的 Python 对象文件应该总是以二进制模式打开。
一些情况下是不能 pickle 的:
- 编译过的代码对象:pickle 中的函数和类只是记录了它们的名字和所在的模块,以便后续使用时重新导入并自动应用模块中的变化;
- 不可导入的类实例:简短地说,就是在实例被加载时,类必须是可导入的;
- 用 C 写的,或者和操作系统瞬间状态相关的一些内置和自定义的类型(比如打开的文件对象)。
使用示例:
>>> import pickle
>>> conf = {"user": "user name", "password": "password"}
>>> pickle.dumps(conf)
b'\x80\x03}q\x00(X\x04\x00\x00\x00userq\x01X\t\x00\x00\x00user nameq\x02X\x08\x00\x00\x00passwordq\x03h\x03u.'
>>> pickle.dumps(conf, protocol=0)
b'(dp0\nVuser\np1\nVuser name\np2\nsVpassword\np3\ng3\ns.'
>>> x = pickle.dumps(conf)
>>> pickle.loads(x)
{'user': 'user name', 'password': 'password'}
>>> y = pickle.loads(x)
>>> y == conf, x is conf
(True, False)
Shelve 文件
shelve
是一种可以使用键来存储和检索任意 Python 对象的文件,并且是 Python 原生支持的。它是 DBM 和pickle
的结合体:- 当存储内存中的对象到文件时,
shelve
模块会首先使用pickle
模块将对象序列化,然后使用DBM
模块根据键将对象字符串存储到 DBM 文件中; - 根据键取出对象时,
shelve
首先使用dbm
模块取出键对应的字符串,然后使用pickle
模块将字符串反序列化为原始对象。
- 当存储内存中的对象到文件时,
shelve
继承了dbm
的特性,导致它在可移植性上较差!shelve
模块的简单使用例子,完整的代码参见 shelve_demo:
def main():
database = shelve.open('demo_db.shelve')
# now, you can save anything to your disk.
database['stu'] = Student('Chris', 23, 2, 12345)
database['list'] = [x for x in range(10)]
# close it
database.close()
# reload database
database = shelve.open('demo_db.shelve')
# read all saved objects
for each in database.keys():
print('{} => {}'.format(each, database[each]))
# clear all
database.clear()
database.close()
# output
list => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
stu => Name: Chris
Age: 23
Grade: 2
Card Id: 12345
shelve
的约束:- 键必须是字符串;
- 单个键对应的对象具有唯一性;
- 更新:不能采用
data[key].attr = value
的方式,而应当采用下面的方式
# 取回 obj = data[key] obj.attr = value # 写回去 data[key] = obj
- 不支持同步更新:实际上是可以同步读取的,但是不能有多个进程同时对一个
shelve
文件写入,否则可能会损坏数据。 - 底层 DBM 格式可能会影响移植性。
面向对象的数据库 ZODB
- ZODB (Zope Object Database),它是一个 Python 独有的全功能的面向对象的数据库系统,可以将 ZODB 视作比
shelve
更强大的选择。虽然 ZODB 不支持 SQL,但是ZODB存储的对象可以利用 Python 的全部威力。 ZODB 重要特性:
- 同步更新: 如果有许多潜在的同步写需求,使用 ZODB 时,无需手动锁定文件防止数据受损。
- 事务提交和回滚: 除非显式提交,否则做出的改变并不生效。这样即使程序崩溃,也不会造成不一致。
- 对象自动更新: ZODB 继承自
Persistent
超类对象,如果属性变动,则会自动更新。 - 对象自动缓存: 出于性能的考虑,对象会被缓存在内存中,并在其不需要使用时自动从内存中清除。
- 平台无关的存储: 由于 ZODB 将数据库存储在独立的扁平文件中,在支持大文件的系统下,可以避免潜在的文件大小限制和
shelve
中 DBM 文件具体实现的差别。
给 Python 3 安装 ZODB:
sudo pip3 install ZODB
。- 下面是使用 ZODB 的模板代码,即创建数据库文件(如果不存在的话),然后连接到数据库,并获取根对象,我们需要在根对象中操作存储,完整的代码参见 zodb_demo:
# `FileStorage` 实际上是将一个数据库映射到无格式文本文件的代理对象。
# 使用 ZODB 的一般步骤
self._storage = FileStorage.FileStorage(db_file)
self._db = DB(self._storage)
self._root = self._db.open().root()
# 存储对象
stuff_db.root['foo1'] = 'foo bar hello, world'
stuff_db.root['foo2'] = {'hello': 'world'}
stuff_db.root['foo3'] = [[1, 2, 3, 4], 'hello']
# 事务提交,然后存储到文件中,最后可以关闭数据库
transaction.commit()
self._storage.close()
# 输出结果:
foo2 => {'hello': 'world'}
foo1 => foo bar hello, world
foo3 => [[1, 2, 3, 4], 'hello']
foo2 => {'hello': 'world'}
foo1 => foo bar hello, world
foo3 => [[1, 2, 3, 4], 'hello']
foo2 => {'hello': 'world'}
foo1 => Boy
foo3 => [[1, 2, 3, 4], 'hello']
# 生成的文件:stuff.fs, stuf.fs.index, stuff.fs.lock, stuff.fs.tmp
SQL 数据库接口
- SQLite 是Python 自带的关系型数据库。由于 Python 社区定义了通用的数据库操作 API,所以一般可以借助SQLite 做原型开发,后期进行修改后可以轻松地更换成其他的数据库:MySQL, Oracle等。
在数据库 API 中, Python 中的 SQL 数据库基于下面三个概念:
- 连接对象:代表一个到数据库的连接,是提交和回滚操作的接口,提供数据库软件包的细节信息,生成游标对象;
- 游标对象:代表了作为字符串提交的 SQL 语句,可以用来访问恩和遍历 SQL 语句的执行结果;
- 查询 SQL select 语句的结果。
此外,数据库 API 还定义了一套数据库异常类型的标准集合,特殊的数据库类型对象构造器,以及顶级信息查看调用,包括线程安全和风格替换检查。
- 重要的 API 使用归纳:
使用方式 | 说明 |
---|---|
conn = connect("username/password") | 登录数据库服务器,并返回一个连接对象 |
conn.close() | 关闭数据库连接 |
conn.commit() | 提交事务 |
conn.rollback() | 回滚未决定的事务 |
cursor = conn.cursor() | 获取游标对象,这样你就可以执行 SQL 了。 |
cursor.execute(sqlstr, [, params]) | 执行 SQL 语句。运行后,游标的rowcount 属性返回更改的行数等。description 首先可以记录表中的字段名称和字段属性。 |
cursor.fetchone() , cursor.fetchmany([size]) , cursor.fetchall() | 获取查询结果 |
- 演示使用 Python 数据库 API 的基本方法示例参见 sqlite_demo.py。在这个示例中,简要介绍了以下几种操作(主要还是需要熟悉 SQL):
- 与sqlite数据库连接,并创建数据库表;
- 演示如何插入记录的三种方法;
- 如何进行查询操作;
- 如何进行记录的更新;
- 如何删除记录。
- 查询后的结果是一个元组,不方便我们使用。所以,一个比较简单的方法是,将返回的结果转换成字典,详细的代码示例参见 sqlite_advance.py:
# 获取列名
col_names = [x[0] for x in cursor.description]
# 获取一行
row = cursor.fetchone()
# 打包成字典
dict_result = dict(zip(col_names, row))
- 将 Python 语言和 SQL 结合起来使用,会非常高效。