基于BTree的ZODB友好的列表实现
项目描述
概述
这个包中的序列具有类似列表的API,但将其值存储在单独的桶中。这意味着对于大型序列中的小变化,序列可能会带来很大的优势。例如,一个基于有序BTree的容器可能希望在序列中存储顺序,这样移动操作只会在数据库中重写一个或两个桶(大约50个字符串或更少),而不是整个内容(例如,可能是数千个字符串)。
如果序列通常是完全重新排列的,那么这个包中的代码的复杂性是不理想的。只有在更改通常相当小的情况下才合理。
一个缺点是读取和写入比普通列表要复杂。如果这实际上要取得进展,可能将其中一些或全部用C编写会有所帮助。然而,它仍然看起来相当快。
另一个缺点是上述桶优势的必然结果:对于更多持久化对象,遍历它将填满大量的ZODB对象缓存(该缓存基于缓存的物体数量,而不是大小)。如果你使用这些存储大量数据并且频繁遍历或更改,请考虑指定一个大的对象缓存。
这些序列返回切片作为迭代器,并添加一些有用的迭代方法。它添加了一个复制方法,提供了blist的廉价复制,共享所有桶和索引,直到发生写入操作,此时它复制并修改受影响的索引和桶。
我们将简要了解这些差异如何工作,然后描述实现的基本机制,最后简要讨论性能特性。
与Python列表的不同之处
切片是迭代器
这不需要太多讨论。获取各种切片会返回迭代器。
>>> from zc.blist import BList >>> l = BList(range(1000)) >>> l[345:351] # doctest: +ELLIPSIS <generator object at ...> >>> list(l[345:351]) [345, 346, 347, 348, 349, 350]>>> l[351:345:-1] # doctest: +ELLIPSIS <generator object at ...> >>> list(l[351:345:-1]) [351, 350, 349, 348, 347, 346]>>> l[345:351:2] # doctest: +ELLIPSIS <generator object at ...> >>> list(l[345:351:2]) [345, 347, 349]
额外的迭代方法
iterReversed 允许您以给定的起始点高效地反向遍历列表。它用于步长为 -1 的切片。
>>> i = l.iterReversed() >>> i.next() 999 >>> i.next() 998 >>> list(i)[-10:] [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
iterSlice 允许您使用切片遍历列表。它相当于使用切片调用 __getitem__。
>>> i = l.iterSlice(345, 351, 2) >>> i # doctest: +ELLIPSIS <generator object at ...> >>> list(i) [345, 347, 349]
便宜的复制
copy 方法生成给定 blist 的廉价副本。所有桶和索引在任一侧发生变化之前都是共享的。可以安全地复制其他副本。
>>> c = l.copy() >>> l == c True >>> list(c) == list(l) True >>> del c[10:] >>> list(l) == range(1000) True >>> list(c) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> l == c False >>> c2 = c.copy() >>> c2 == c True >>> list(c2) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
机制
在其实现中,序列是一个改进的 B+ 树。索引是键,但每个桶或分支从 0 开始。例如,一个完全平衡的包含 16 个元素的桶序列,桶或分支中最多有 3 个条目,其“键”如下。在图中,前三行是索引,最后一行是桶。
0 8 0 4 0 4 0 2 0 2 0 2 0 2 01 01 01 01 01 01 01 01
例如,您会通过以下过程获得位置 5 的值
在顶层索引(第一行,键为 0 和 8)中,找到小于所需位置的最大键,并使用关联的值(索引或桶,在这种情况下是第二行中由第一对 0 和 4 表示的索引)作为下一步。在这种情况下,顶层索引的键为 0 和 8,因此小于位置 5 的最大键是 0。从位置中减去此键以进行下一步。这个差值将用作下一步的位置。在这种情况下,下一步的位置将是(5-0=)5。
下一个索引具有键 0 和 4。小于 5 的最大键是 4。使用与 4 键关联的子索引进行下一步(第三行中的第二对 0 和 2),并从位置(5)减去键(4)以确定下一步要使用的位置(=1)。
下一个索引(第三行中的第二对 0 和 2)需要找到位置 1。这将返回最后一行中的第三对 0 1。新位置将是(1-0=)1。
最后,底部桶中的位置 1 存储了实际所需的值。
这种安排最小化了在序列中插入新值时对键的更改:忽略树的平衡,只需调整父项及其后续兄弟项。例如,在上述桶序列的 0 位置插入新值(从算法的角度来看,这是最坏的情况,涉及到的对象数量最多)会导致以下树
0 9 0 5 0 4 0 3 0 2 0 2 0 2 012 01 01 01 01 01 01 01
性能特征
优点
__getitem__ 是高效的,不会加载不必要的桶。它处理切片也相当不错,即使切片非常大,也不会加载中间桶。切片目前返回可迭代对象而不是列表;这可能会切换到某种视图。现在应该假定的是,您可以遍历切片的结果。
__setitem__ 和所有写方法在高效加载桶和仅写入所需内容方面做得相当不错,并支持完整的 Python 切片语义。
copy 是廉价的:它重用桶和索引,以便在它们发生变化时懒惰地创建新的内部组件。
虽然 __contains__、__iter__、index 和其他方法是暴力方法并使用 Python 编写,但它们可能不会加载所有桶和项目,而使用常规列表或元组,它们总是会这样做。另请参阅 iter、iterReversed 和 iterSlice。
一般之处
count、__eq__ 和其他方法会加载所有桶和项目,并且是暴力搜索,在 Python 中。相比之下,列表和元组会加载所有项目(更差),在 C 中是暴力搜索(较好,但不是算法上的)。
缺点
这将为一个 blist 创建许多持久化对象,这可能会根据环境和使用情况导致缓存淘汰问题。
我提到过这是在 Python 中,而不是 C 中吗?至少这是可修复的,实际上目前似乎没有太大问题,至少对于作者的用法来说是这样。
变更
1.0b2 (2009-03-12)
修复:内部数据结构在 ZODB 中存储不正确,因此从新的数据库连接加载的 BLists 会损坏。
移除了未使用的代码和对 rwproperty 的依赖。
改进了 BList API 的测试覆盖率,修复了对索引 -1 或有效索引范围末端的项的访问。
1.0b1 (2008-10-06)
初始版本。
项目详情
zc.blist-1.0b2.tar.gz 的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | f257f02e8c5ec55467e64c8d2c21c1d9471b83fba0377560d98f0340ed1925ed |
|
MD5 | 335eb7fa3b8a5a3ac13dc37ac4337e76 |
|
BLAKE2b-256 | dec02ad9b2b28282cfaaa9e9715e8909071ef719f3ab0fa695766b7548b6665b |