python序列化模块-shelve模块详解
shelve:vt. 将(书等)放置在架子上;搁置,将某事放到一旁不予考虑;将…搁在一边;装搁架于;
个人感觉有点像字典缓存?暂时搁置到一旁的意思?
研究了一段时间后,感觉它就是当成了一种临时的数据库(dbm)缓存文件来用的感觉。
为什么用shelve?
(特别是在已有json和pickle的情况下)
使用json或者pickle持久化数据,能dump多次,但load的话只能取到最新的dump,
因为先前的数据已经被后面dump的数据覆盖掉了。
如果想要实现dump多次不被覆盖,就可以想到使用shelve模块。
shelve模块可以持久化所有pickle所支持的数据类型。
另外,写程序的时候如果不想用关系数据库那么重量级的去存储数据,也可以用到shelve。
shelf也是用key来访问的,使用起来和字典类似。
注意的是,在shelve模块中,key必须为字符串,而值可以是python所支持的数据类型。
另外,shelve其实用anydbm去创建DB并且管理持久化对象的。
shelve只提供给我们一个open方法,是用key来访问的,使用起来和字典类似。
可以像字典一样使用get来获取数据等。
shelve.py中的open方法代码如下:
def open(filename, flag='c', protocol=None, writeback=False):
"""Open a persistent dictionary for reading and writing.
# 打开一个持久的字典,用于阅读和写作。
The filename parameter is the base filename for the underlying
database. As a side-effect, an extension may be added to the
filename and more than one file may be created. The optional flag
parameter has the same interpretation as the flag parameter of
dbm.open(). The optional protocol parameter specifies the
version of the pickle protocol (0, 1, or 2).
See the module's __doc__ string for an overview of the interface.
"""
return DbfilenameShelf(filename, flag, protocol, writeback)
源码中的一些有关说明摘录:
To summarize the interface (key is a string, data is an arbitrary
object):
import shelve
d = shelve.open(filename) # open, with (g)dbm filename -- no suffix
d[key] = data # store data at key (overwrites old data if
# using an existing key)
文件句柄[key] = 你想存储的数据 #存储数据在键(如果使用现有的key,将会覆盖旧数据)
data = d[key] # retrieve a COPY of the data at key (raise
# KeyError if no such key) -- NOTE that this
# access returns a *copy* of the entry!
在键上检索数据的副本(如果没有这样的键,就会抛出键错误)——注意,这个访问返回了条目的*拷贝* !
del d[key] # delete data stored at key (raises KeyError
# if no such key)
删除存储在key中的数据(如果没有这样的键,就会出现KeyError)
flag = key in d # true if the key exists 如果键存在,则为真。
list = d.keys() # a list of all existing keys (slow!) 所有现有键的列表(注意:缓慢!)
d.close() # close it 关闭文件句柄
上面的说明我们主要要注重到 d[key] = data 和 data = d[key],这俩充分说明了shelve的一些机制。
首先,shelve做为一个类似数据库缓存的大字典,肯定得支持用户对它写入一些键,这个很好理解。
但是,如果你在shelve db中己存在有一个key,你重新再写入与它同名的key的一些数据(data),
那新写入的这个覆盖掉旧的同样也是很好理解的。
那么不好理解的就是,为什么你对一个key里面的值去作出增加或删除其中一个元素的时候会修改不成功?
这里在data = d[key]里其实就给出了答案,这种属于对key的访问,返回的其实是条目(key)的一个拷贝!
所以才有了writeback这个默认参数的存在,让你可以自主选择要不要做出修改后,将拷贝写回!
这样就可以修改生效。
例:
import shelve
db1 = shelve.open('shelve_db1')
db1['dic'] = {'int':12, 'float':2.5, 'string':'shelve db'}
#直接对文件句柄[key]操作,就可以存入数据
db1.close()
且重要的是它还会直接在打开的当前目录生成三个文件:
shelve.db1.bak
shelve.db1.dat
shelve.db1.dir
其中shelve.db1.dat 存储的就是b字节数据类型的数据,
bak和dir后缀的就可能是和数据库相关的设计缓存之类的东西了。
注:文件生成后,我们可以将前面的这一段生成shelve db的代码先行注释掉,不影响下面的继续测试操作。
db1 = shelve.open('shelve_db1')
existing = db1['dic']
# 取出数据的时候也只需要直接用字典的操作方法获取即可,但是如果key不存在会报错
db1.close()
print(type(existing), existing)
# <class 'dict'> {'string': 'Shelve db', 'float': 2.5, 'int': 12}
shelve模块有个限制,它不支持多个应用同一时间往同一个DB(文件)进行写操作。
所以如果只需进行读操作,可以修改默认参数flag='r' 让shelve通过只读方式打开DB(文件)。
注:经测试,目前发现的是r模式在python2.7环境下可以生效,
但python3.5不生效,很可能与python3.5不在存在anydbm模块有些关系,
python2.7中是存在anydbm的,而shelve实际上是anydbm的加强版,有可能就是dbm这里产生的问题了。
db1 = shelve.open('shelve_db1', flag='r')
res1 = db1['dic']['int']
db1.close()
print(type(res1), res1)
# <class 'int'> 12
由于shelve在默认情况下是不会记录对持久化对象(字典下的键的值-条目)做出修改的,
所以在shelve.open()时候需要修改默认参数writeback=True,
否则对象的条目修改不会'拷贝回写'来进行保存。
import shelve
db1 = shelve.open('shelve_db1', writeback=True)
res2 = db1['dic']['date'] = '2018-4-20'
db1.close()
print(type(res2), res2)
# <class 'str'> 2018-4-20
当试图让shelve去自动捕获对象的变化时,应当在打开shelf的时候将writeback设置为True。
而将writeback这个flag设置为True以后,shelf将会将所有从DB中读取的对象存放到一个内存缓存。
当close() shelf的时候,缓存中所有的对象会被重新写入DB。
关于key的数据类型:
import shelve
db2 = shelve.open('shelve_db2.dat')
db2[(1, 2)] = {'lv1':'枪兵'}
print(type(db2[(1, 2)]), db2[(1, 2)])
db2.close()
以上会产生报错:AttributeError: 'tuple' object has no attribute 'encode'
虽然看似shelve db是一个字典,但它的key得支持encode方法,所以
在shelve模块中,key必须为字符串,而值可以是python所支持的数据类型。
例:更详尽的测试说明
import shelve
list1 = ['tie', 'le', 'yu']
# 既然最终生成的文件会是dat格式的,何不一开始就指定后缀是dat
db2 = shelve.open('shelve_db2.dat')
db2['lis'] = list1
# 文件句柄是通过字典的操作方式去拿里面的键值对,lis这个键对应的值是一个列表
db2['lis'].append('mao')
# 而此列表增加一个字符串元素后再打印,感觉不出有发生增加的变化
print(type(db2['lis']), db2['lis'])
# 返回列表:['tie', 'le', 'yu']
没有'mao' ,存储的'mao'到哪里去了呢?
其实很简单,lis 并没有写回,虽然把['tie','le','yu']存到了lis,
但当你再次读取db2['lis']的时候,db2['lis']只是一个拷贝,
而你没有用默认参数writeback将拷贝写回,
当你再次读取db2['lis']的时候,它又从数据源中读取了一个拷贝,
所以,你新修改的内容并不会出现在拷贝中,解决的办法最方便的就是使用默认参数writeback=True,
然后还有一个方法是利用中间缓存的变量,如下所示:
利用中间变量
import shelve
list2 = ['tie', 'le', 'yu']
db2 = shelve.open('shelve_db2.dat')
db2['lis'] = list2
temp = db2['lis']
temp.append('mao')
db2['lis'] = temp # 这种属于直接赋值和拷贝写回无关,会生效
print(type(db2['lis']), db2['lis'])
返回的结果中有 <class 'list'> ['tie', 'le', 'yu', 'mao']
直接修改默认参数writeback=True 如下:
import shelve
list3 = ['a', 'b', 'c']
db2 = shelve.open('shelve_db2.dat', writeback=True)
db2['lis2'] = list3
db2['lis2'].append('d')
for k, v in db2.items():
print(k, v)
# 显示从测试开始存在shelve_db2.dat数据文件中键和值如下,可以看到lis2也是成功添加了'd'的。
# lis2 ['a', 'b', 'c', 'd']
# dic ['tie', 'le', 'yu']
# lis ['tie', 'le', 'yu', 'mao']
db2.close()
import shelve
db2 = shelve.open('shelve_db2.dat')
db2['lis2'] = ['1', '2', '3'] # 这是直接赋值,新列表覆盖掉旧列表, 所以并不需要用到回写参数
for k, v in db2.items():
print(k, v)
# 显示如下
# lis ['tie', 'le', 'yu', 'mao']
# lis2 ['1', '2', '3']
# dic ['tie', 'le', 'yu']
db2.close()
同理,以下想对字典进行添加的操作,实际上也是拷贝没有写回,所以看起来没有保存修改成功一样
import shelve
db2 = shelve.open('shelve_db2.dat')
db2['dic2'] = {'name':'铁乐', 'age':18, 'sex':'男'}
db2['dic2']['hobby'] = ['下棋']
# 此时虽然看似添加了一个新键值对,其实并没有做写回操作,
# 下面再做打印操作时,显示的还是从源中取出的一个拷贝,不会有显示增加的键值
print(type(db2['dic2']), db2['dic2'])
db2.close()
# 显示<class 'dict'> {'sex': '男', 'age': 18, 'name': '铁乐'}
所以我们一定要弄明白一件事情,
从shelve的db文件中重新再访问一个key拿的是它的拷贝!
修改此拷贝后不做拷贝写回并不影响原来的key,
但你要是直接做的操作是赋值新的值到一个key里,那肯定就是指向原来的key,会被覆盖的。
而这种赋值覆盖对于shelve来说这是一个正常的行为阿。
和键中的值看起来不能被修改一事并不矛盾。
writeback方式有优点也有缺点。
优点是减少了我们出错的概率,且让对象的持久化对用户更加的透明了;
但这种方式并不是所有的情况下都需要,
首先,使用writeback以后,shelf在open()的时候会增加额外的内存消耗,
并且当DB在close()的时候会将缓存中的每一个对象都写入到DB,这也会带来额外的等待时间。
因为shelve没有办法知道缓存中哪些对象修改了,哪些对象没有修改,因此所有的对象都会被写入。
应用场景例子:
模拟保存用户登录状态:
距离上一次登录不超过设置时间内的可以重新登录,
超过时间则无法再使用原用户密码登录。
又需要重新注册。
import time
import datetime
import hashlib
import shelve
# 模拟一个网站登录,新用户先进行注册再登录,
# 旧用户登录判断登录的时间,离上一次登录时间超过多长时间的就再也不能登录了。
# 只适用于一些开放的临时登录的场景?
# 测试时设置登录超时的时间为6分钟,实际应用可以设时间久一点
LOGIN_TIME_OUT = 0.60
# 设置一个临时db,且允许拷贝写回
db = shelve.open('user_shelve.db', writeback=True)
# 新用户登录函数,后面测试后发现其实就是相当于新注册一个用户!
def newuser():
prompt = "login desired: " # prompt,提示
while True:
name = input(prompt).strip()
if name in db:
prompt = "name taken, try another: " # 用户己存在,请重新输入
continue
elif len(name) == 0:
prompt = "name should not be empty, try another: " # 用户名不应该是空的,请重新输入
continue
else:
break
pwd = input("password: ").strip()
db[name] = {"password": md5_digest(pwd), "last_login_time": time.time()}
# 判断用户有没有已存在登录及记录上一次登录时间的函数(现有用户)
def olduser():
name = input("login: ").strip()
pwd = input("password: ").strip()
try:
password = db.get(name).get('password')
# 捕获一个异常,试图访问一个对象没有的属性,也就是处理用户输入不存在的用户时
except AttributeError:
print(" 33[1;31;40mUsername '%s' doesn't existed 33[0m" % name)
# 提示用户不存在
return
if md5_digest(pwd) == password:
login_time = time.time() # 当前登录时间
print(login_time)
last_login_time = db.get(name).get('last_login_time') # 上一次登录时间
print(last_login_time)
if login_time - last_login_time < LOGIN_TIME_OUT: # 如果登录没有超时
print(" 33[1;31;40mYou already logged in at: <%s>