- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
导读
shelve是 Python 标准库中一个将对象序列化后持久化到磁盘的模块,它提供一种"持久化的字典"(persistent dictionary),让开发者可以用读写普通 dict 的方式保存任意 Python 对象。本文以 KBEngine 仓库内嵌的 CPython 3.7.3 发行版(Lib/shelve.py 及其官方文档 Doc/library/shelve.rst)为主体,完整讲解shelve.open的参数语义、三个核心类、可变对象写入陷阱、同步与关闭机制,并结合源码与测试用例(Lib/test/test_shelve.py)深入剖析其底层实现,帮助读者在游戏服务器脚本或其他 Python 场景中安全、高效地使用 shelf 存储。
什么是 shelf:与 dbm 的本质区别
shelve模块在文档中开宗明义:shelf 是一个持久化的、类字典对象。它与dbm数据库的关键差异在于:
- dbm:键和值都只能是字符串或字节串;
- shelf:键是普通字符串,值可以是任意 Python 对象——包括大多数类实例、递归数据类型、包含大量共享子对象的对象,只要是
pickle模块能够处理的对象即可。
原因在于 shelf 的底层存储机制:值先经pickle序列化为字节串,再交给 dbm 风格的数据库保存;读取时再反序列化回 Python 对象。模块源码开头的 docstring 与实现 Lib/shelve.py 完全一致地说明了这一点,其核心序列化依赖正是from pickle import Pickler, Unpickler与from io import BytesIO。
在 KBEngine 项目中,该模块位于服务器进程内嵌的 Python 解释器环境中。引擎通过 src/lib/pyscript/script.cpp 中的Script::install()(调用Py_SetPythonHome、Py_SetPath、Py_Initialize等)启动内置解释器,其版本由 Include/patchlevel.h 定义为PY_VERSION "3.7.3"。因此本仓库中 shelve 的 API 行为以 Python 3.7.3 为准(例如open()不暴露keyencoding参数,但Shelf类自 3.2 起支持该参数)。
核心函数:shelve.open(filename, flag='c', protocol=None, writeback=False)
import shelve db = shelve.open('my_shelf')shelve.open用于打开一个持久化字典,其签名与文档一致:
| 参数 | 默认值 | 含义 |
|---|---|---|
filename | 必填 | 底层数据库的基础文件名。副作用是:底层库可能为文件追加扩展名,甚至生成多个文件 |
flag | 'c' | 与dbm.open的flag参数语义相同:'r'只读、'w'读写、'c'读写且必要时创建、'n'总是新建空库 |
protocol | None(即 3) | 序列化值所用的 pickle 协议版本;默认使用版本 3 的 pickle |
writeback | False | 是否开启写回缓存,决定可变条目的修改能否被自动感知(见下文陷阱一节) |
源码层面,open()只是DbfilenameShelf的便捷封装(Lib/shelve.py):
def open(filename, flag='c', protocol=None, writeback=False): return DbfilenameShelf(filename, flag, protocol, writeback)而DbfilenameShelf.__init__内部调用dbm.open(filename, flag)获得底层数据库对象,再交给Shelf构造器(Lib/shelve.py)。关于flag的完整语义与dbm.whichdb的探测行为,可对照同目录下的 Doc/library/dbm.rst。
协议版本与默认值
在Shelf.__init__中,protocol为None时会被显式赋值为3(Lib/shelve.py):
if protocol is None: protocol = 3 self._protocol = protocol这一默认值也被测试用例验证:test_default_protocol断言shelve.Shelf({})._protocol == 3。协议版本的选择直接影响序列化字节流的大小与兼容性——版本 0 为文本格式(ASCII),版本 1 引入二进制格式,版本 2 支持类与实例更高效的序列化,版本 3 起支持bytes对象等新特性(详见 Doc/library/pickle.rst)。测试 Lib/test/test_shelve.py 中的test_ascii_file_shelf、test_binary_file_shelf、test_proto2_file_shelf分别以protocol=0/1/2打开文件型 shelf 并读写元组数据,验证各协议下的可用性。
三个核心类:Shelf、BsdDbShelf、DbfilenameShelf
Shelf(dict, protocol=None, writeback=False, keyencoding='utf-8')
Shelf是 shelf 实现的基类,是collections.abc.MutableMapping的子类,将 pickle 后的值存入传入的dict 对象(任意类字典对象)中。参数:
dict:底层类字典存储,键必须是字节串;protocol:pickle 协议版本,默认 3;writeback:为True时缓存所有访问过的条目,并在sync()/close()时写回;keyencoding:写入底层 dict 前对键使用的编码,默认'utf-8'。该参数自 Python 3.2 加入,此前键固定按 UTF-8 编码;3.4 起Shelf支持上下文管理器协议。
源码中键的编解码发生在每个魔术方法里:__contains__、__getitem__、__setitem__、__delitem__都通过key.encode(self.keyencoding)转换键,而__iter__则用k.decode(self.keyencoding)还原(Lib/shelve.py)。测试test_keyencoding验证了 UTF-8 默认编码、可替换为latin-1,以及选用ascii时对非 ASCII 键抛出UnicodeEncodeError的行为。
BsdDbShelf(dict, protocol=None, writeback=False, keyencoding='utf-8')
BsdDbShelf是Shelf的子类,额外暴露first()、next()、previous()、last()与set_location()五个游标式遍历方法。这些方法仅存在于第三方bsddb模块(如 pybsddb),其他 dbm 后端没有对应物。使用它时传入的 dict 必须支持这些方法,通常由bsddb.hashopen、bsddb.btopen或bsddb.rnopen打开。源码实现中每个方法都从底层 dict 取出原始字节,经key.decode(self.keyencoding)还原键、Unpickler(f).load()还原值(Lib/shelve.py)。
DbfilenameShelf(filename, flag='c', protocol=None, writeback=False)
DbfilenameShelf是Shelf的子类,接受文件名而非 dict 对象,内部用dbm.open(filename, flag)打开底层文件。默认情况下文件会被创建并同时以读写方式打开。它是shelve.open()直接返回的类型,也是日常使用最多的一条路径。
sync()与close():两个额外的字典方法
shelf 对象支持字典的全部方法,此外还有两个额外方法:
Shelf.sync():若以writeback=True打开,将缓存中所有条目写回;同时清空缓存,并在可行时同步底层持久化字典到磁盘。close()会自动调用它。Shelf.close():同步并关闭底层持久化字典。对已关闭的 shelf 执行操作会抛出ValueError。
源码中,sync()的实现很巧妙(Lib/shelve.py):
def sync(self): if self.writeback and self.cache: self.writeback = False for key, entry in self.cache.items(): self[key] = entry self.writeback = True self.cache = {} if hasattr(self.dict, 'sync'): self.dict.sync()先临时关闭writeback标志再逐项写回,是为了避免写回过程再次把条目塞回缓存;写回完成后再清空缓存。而close()会先把self.dict替换为_ClosedDict标记对象——_ClosedDict的所有操作(__iter__、__len__、__getitem__等)统一抛出ValueError('invalid operation on closed shelf')(Lib/shelve.py),这就是"关闭后操作失败"的机制来源。测试test_close与test_with均验证了关闭后访问会抛ValueError。
关键陷阱:可变对象的写入语义与 writeback
shelf 无法感知持久化字典中的可变条目何时被就地修改。默认情况下(writeback=False),修改后的对象只有在重新赋值给 shelf 时才会写入磁盘。这一点无论文档还是源码都反复强调,是使用 shelf 最常见的坑。
经典示例(完整保留自官方文档)
import shelve d = shelve.open(filename) # 打开——底层库可能给文件名追加后缀 d[key] = data # 在 key 处存储 data(若 key 已存在则覆盖旧数据) data = d[key] # 取回 key 处数据的一个"副本"(不存在则抛 KeyError) del d[key] # 删除 key 处数据(不存在则抛 KeyError) flag = key in d # 判断 key 是否存在 klist = list(d.keys()) # 列出所有现有 key(很慢!) # 因为 d 是以 writeback=False 打开的,请务必注意: d['xx'] = [0, 1, 2] # 这行符合预期,但是…… d['xx'].append(3) # *这行不生效!* —— d['xx'] 仍然等于 [0, 1, 2]! # 以 writeback=False 打开时,你需要这样小心地写: temp = d['xx'] # 取出副本 temp.append(5) # 修改副本 d['xx'] = temp # 把副本重新存回去,才能持久化 # 或者 d = shelve.open(filename, writeback=True) 会让你直接写 # d['xx'].append(5) 也能生效,但代价是占用更多内存、close() 更慢。 d.close() # 关闭源码层面的原因
__getitem__从底层 dict 取出字节串后,用Unpickler(BytesIO(...)).load()反序列化出一个全新的对象返回(Lib/shelve.py)。调用方对这个返回值的就地修改,并不会反向影响底层存储中的字节串——除非重新d[key] = temp触发__setitem__的Pickler(...).dump(value)重新序列化。
测试test_mutable_entry精确复现了这一行为:
writeback=False时,s['key1']存入[1,2,3,4]后执行s['key1'].append(5),再次读取仍为[1,2,3,4];writeback=True时,同样的代码读出来是[1,2,3,4,5]。
开启 writeback 的代价
writeback=True时,所有访问过的条目都会缓存在内存(self.cache)中,并在sync()/close()时全部写回。由于 shelf 无法判断哪些条目可变、哪些被真正修改过,只能缓存并回写所有访问过的条目,因此:
- 若访问的条目很多,缓存会消耗大量内存;
close()会变得很慢,因为要写回所有被访问过的条目。
测试test_writeback_also_writes_immediately还验证了一个容易被忽略的细节:writeback=True时,d[key] = value的赋值本身也会立即写入底层存储(底层字节串已更新),只是同时保留一份缓存用于后续感知修改。
上下文管理器:推荐的安全关闭方式
官方文档明确告诫:不要依赖 shelf 被自动关闭,不需要时应显式调用close(),或者把shelve.open用作上下文管理器:
with shelve.open('spam') as db: db['eggs'] = 'eggs'上下文管理器支持自 Python 3.4 加入。实现上是Shelf定义了__enter__(返回自身)与__exit__(调用self.close())(Lib/shelve.py)。即使不显式关闭,Shelf的__del__也会尝试兜底关闭,但解释器关闭期间close()可能抛错,因此代码中通过try/except保证self.dict至少被替换为_ClosedDict或None(Lib/shelve.py)。测试test_with验证了with块结束后再访问 shelf 会抛ValueError。
安全警告:不要从不可信来源加载 shelf
因为 shelf 底层由pickle支撑,从不可信来源加载 shelf 是不安全的。与直接使用pickle.load一样,加载一个精心构造的 shelf 文件可能执行任意代码。官方文档以 warning 形式强调此点,这一点在 Lib/shelve.py 的 docstring 中同样有体现。在 KBEngine 这类多进程、多用户可触及资源的服务器环境中,尤其要注意只加载自己生成、可受信任的 shelf 文件。
使用限制:后端差异、体积与并发
- 后端选择不确定:shelf 使用哪个数据库包(
dbm.ndbm或dbm.gnu)取决于当前环境有哪些接口可用。因此不要直接用dbm打开 shelf 文件——你无法预先确定文件对应的后端。推荐用dbm.whichdb(filename)探测格式(其返回值如'dbm.ndbm'、'dbm.gnu'或空字符串/None,见 Doc/library/dbm.rst)。若环境中没有dbm.gnu/dbm.ndbm,则会回退到慢但简单的dbm.dumb实现。 - 受 dbm 自身限制:如果底层使用 dbm,存储对象的 pickle 表示应当相当小;极少数情况下键冲突可能导致数据库拒绝更新。
- 不支持并发读写:shelf不支持对同一批对象的并发读写(多个进程同时只读是安全的)。当某个程序以写模式打开 shelf 时,其他程序不应再以读或写模式打开它。解决方案是 Unix 文件锁,但不同 Unix 版本差异很大,且需要了解底层数据库实现。
在 KBEngine 仓库中查看与验证
- 模块源码:kbe/src/lib/python/Lib/shelve.py ——
open、Shelf、BsdDbShelf、DbfilenameShelf、sync、close的完整实现; - 官方文档(本主题主体):kbe/src/lib/python/Doc/library/shelve.rst;
- 单元测试:kbe/src/lib/python/Lib/test/test_shelve.py —— 覆盖关闭语义、各 pickle 协议、内存型 shelf、可变条目写回、键编码、上下文管理器等行为,可用
python -m test.test_shelve方式运行验证; - 关联模块:底层 dbm 接口见 Doc/library/dbm.rst,序列化协议见 Doc/library/pickle.rst;
- 内嵌解释器:KBEngine 服务器进程通过 src/lib/pyscript/script.cpp 初始化内置 Python,版本为 Include/patchlevel.h 定义的 3.7.3,
shelve即位于该解释器自带的Lib标准库目录中。
小结
shelve以"字典接口 + pickle 序列化 + dbm 存储"三层结构提供了轻量级的对象持久化方案,适合存放配置对象、缓存数据等中小体量、无并发写需求的场景。使用时的三个要点:牢记读操作返回副本,对可变对象要么显式写回、要么开启writeback(并接受内存与关闭开销);显式close()或使用with;只加载可信来源的 shelf 文件。理解了 Lib/shelve.py 的源码与 test_shelve.py 的测试用例,就能准确预判 shelf 在各种边界条件下的行为。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
KBEngine 内嵌 Python 的 posix/os 模块:POSIX 系统调用、大文件支持与 environ 环境字典解析
KBEngine 内嵌 Python 的 posix/os 模块:POSIX 系统调用、大文件支持与 environ 环境字典解析 KBEngine 的服务器端
游戏开发后端KBEngine 内嵌 Python 的 compileall 模块:批量字节码编译的原理、参数与实战用法
KBEngine 内嵌 Python 的 compileall 模块:批量字节码编译的原理、参数与实战用法 KBEngine 服务端内嵌了 CPython 解释
游戏开发后端KBEngine 内嵌 Python 3.7 的 keyword 模块:关键字检测、kwlist 全表与源码生成机制解析
KBEngine 内嵌 Python 3.7 的 keyword 模块:关键字检测、kwlist 全表与源码生成机制解析 导读 keyword 是 Python
游戏开发后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考