铁乐学python-shelve模块详解

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("\033[1;31;40mUsername '%s' doesn't existed\033[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("\033[1;31;40mYou already logged in at: <%s>\033[0m" % datetime.datetime.fromtimestamp(
                last_login_time).isoformat()) # 显示你准备登录的时间
        db[name]['last_login_time'] = login_time # 写入登录时间到db
        print("\033[1;32;40mwelcome back\033[0m", name) # 显示欢迎回来
    else:
        print("\033[1;31;40mlogin incorrect\033[0m") # 否则显示登录失败

# md5摘要加密传输进来的明文密码
def md5_digest(plain_pass):
    md5 = hashlib.md5()
    md5.update(plain_pass.encode('utf-8'))
    return md5.hexdigest()

# 菜单
def showmenu():
    # 下面菜单分别是新用户登录,当前用户登录,退出
    prompt = """
(N)ew User Login
(E)xisting User Login
(Q)uit
Enter choice: """
    done = False # 完成 默认值false
    while not done:
        chosen = False # 选择 默认值false
        while not chosen:
            try:
                choice = input(prompt).strip()[0].lower()
                # 捕获异常选择直接变成选q退出程序
            except (EOFError, KeyboardInterrupt):
                choice = "q"
            print("\nYou picked: [%s]" % choice) # 提示你的选择是什么
            if choice not in "neq":
                print("invalid option, try again") # 当输入的不为neq时,提示输入有误请重新输入
            else:
                chosen = True # 选择 为真,中断循环

        if choice == "q": done = True # 选择为q,退出,中断循环
        if choice == "n": newuser() # 选择为n,执行newuser()函数
        if choice == "e": olduser() # 选择为e,执行olduser()函数
    db.close() # 关闭db文件句柄

# 执行主程序
showmenu()

效果如下:

(N)ew User Login
(E)xisting User Login
(Q)uit
Enter choice: n

# 之前测试己将tiele与mao用户存在临时db文件中

You picked: [n]
login desired: tiele
name taken, try another: mao  
# 提示用户己存在,请重新再注册一个新用户
name taken, try another: yue
password: 123
# 新注册了一个yue用户,密码123的在db里

(N)ew User Login
(E)xisting User Login
(Q)uit
Enter choice: e

You picked: [e]
login: tiele
password: 123456
welcome back tiele 
# 登录成功
# 后面感觉扩展成不用再输密码,而是直接登录那种可能还贴近一些记住密码的场景?

You picked: [e]
login: le
password: 123
Username 'le' doesn't existed
# 提示用户不存在

You picked: [e]
login: mao
password: 123456
login incorrect  # 离上一次登录时间超时,用户登录失效

总之,大致上就是如此,shelve模块它可以当成一个轻量的数据库db来使用,
比起刚学python的时候使用文件来说,还是比较显得有趣和高大上一些的。
利用它的字典特性,也能玩转一下记录用户登录状态来完成一些例如再进其它页面,
再调用其它函数或方法就能正常调用不需重新认证等,简洁代码。
模拟感受一下有数据库存在的情景等还是比json和pickle有用的。

小结:
1、shelve模块将内存数据以字典的类型(key,value)通过文件持久化,模拟出简单的db效果。
2、shelve模块可以持久化任何pickle可支持的python数据格式,但是它的key必需得是字符串。
3、shelve可以看作是pickle模块的一个封装,但它实现了可以多次dump(后面的dump不会覆盖前面的)和多次load。
4、shelve访问己有key时,实际上取出的是数据源给出的一份拷贝,
所以对于拷贝做出的增加和删除等操作都需要用writeback=True参数才能实现写入回源中进行修改。
5、shelve对于d[key] = data这种操作,视为存储数据,无则新增,有则覆盖,
与访问key对当中的值(条目)进行修改默认不回写并不矛盾和冲突。
6、默认安装环境下,shelve中的r只读模式在python2.7能生效,在3.5环境中则不能生效。
有很大可能是与2.7中存在anydbm模块,而3.5中只存在dbm模块而不存在anydbm有关。

end
2018-4-21

发表评论

电子邮件地址不会被公开。 必填项已用*标注

3 − 2 =