在 ZOBD OOBTree 中使用对象作为键的正确方法是什么?

What is the correct way to use objects as keys in a ZOBD OOBTree?

在 ZOBD 中(在 Python 3.x 中)我希望能够将对象作为键存储在 BTrees.OOBTree.OOBTree() 中。我尝试时遇到的错误示例(请参阅评论):

from BTrees.OOBTree import OOBTree as Btree

class Test:
    pass

bt=Btree()
t=Test()
bt[t]=None #TypeError: Object has default comparison

因此,我在某处读到可能需要定义 __eq__ 才能消除该错误,但尽管这似乎解决了之前的问题,但似乎会导致更多问题。示例:

[编辑:应该注意的是,我在继承 OOBTree(和 TreeSet)时发现了一些问题,就像我在这里所做的那样。显然,他们没有正确保存;因此,它与继承 Persistent 不同,即使它们继承了 Persistent。]

from BTrees.OOBTree import OOBTree as Btree

class Test:
    def __eq__(self, other): #Maybe this isn't the way to define the method
        return self==other

bt=Btree()
t=Test()
bt[t]=None

t in bt #TypeError: unorderable types: Test() < Test()

在 BTree 或 OOBTree 中使用对象作为键的正确方法是什么?我也需要测试密钥是否存在。

对于那些不知道的人来说,ZODB 中的 BTrees 非常类似于可扩展的 Python 字典(它们应该可以使用比常规 Python 字典更多的键值对)设计用于坚持。

我认为 this answer 可以帮助您解决问题。

基本上,您必须在对象上重新实现三个方法:

  1. __eq__(等式检查)
  2. __ne__(不相等检查)
  3. __hash__ 使对象真正可序列化为字典键

虽然 Eliot Berriot 的回答让我得到了我需要的答案,但我认为我会 post 对我有帮助的完整答案,这样其他人就不必花费额外的时间来解决问题。 (我要用第二人称自言自语。)


首先(我并没有真正问过它,但你可能会想这样做),不要继承 OOBTree 或 OOTreeSet(这会导致问题)。制作你自己的 class 继承 Persistent 的 es,如果你想要类似继承的 OOBTree 的东西,并在里面放一个 OOBTree 或 OOTreeSet(另外,如果你想要,定义使它看起来像字典或集合所需的方法那个)。

接下来,您需要创建一个持久 ID 系统(对于您放入 OOBTree 或 OOTreeSet 中的每个对象,因为如果您没有 ZOBD 可以使用的唯一整数,对象会导致 OOBTrees 和 OOTreeSets 发生故障跟踪你的对象。你需要定义 Eliot 提到的方法,以及其他一些类似的方法(这些方法需要比较整数 ID——而不是对象本身);即定义你的 classes 生成的对象将成为 OOBTree 的键或包含在 OOTreeSet 中:__eq____ne____hash____lt____le____gt__,和 __ge__。然而,为了拥有一个持久的 ID,你将不得不制作一个 ID 计数器 class 或其他东西(因为它不会将普通整数保存为值出于某种奇怪的原因在 OOBTree 中,除非我做错了),并且那个计数器 class 也必须有一个 ID。

接下来,你需要确保如果你正在制作对象键,那么你最好不要在同一个 OOBTree 中将字符串之类的东西也作为键,否则你会遇到神秘的问题(由于字符串没有与您的对象相同的 ID 系统)。它会将字符串键与您的对象键进行比较,并导致错误,因为它们不是为了比较而设计的。

这是一个 Python 3.x 代码的工作示例,它允许您将对象用作 OOBTree 中的键,并且它将允许您迭代 OOBTree 中的持久对象(并使用它们作为键)。它还向您展示了如何保存和加载对象。

抱歉,它有点长,但它应该能让您很好地了解它是如何工作的:

import transaction, ZODB, ZODB.FileStorage
from persistent import Persistent
from BTrees.OOBTree import OOBTree as OOBTree
from BTrees.OOBTree import OOTreeSet as OOTreeSet

class Btree(Persistent):
    def __init__(self, ID=None, **attr):
        #I like to use entirely uppercase variables to represent ones you aren't supposed to access outside of the class (because it doesn't have the restrictions that adding _ and __ to the beginning do, and because you don't really need all caps for constants in Python)
        Persistent.__init__(self)
        self.DS=OOBTree() #DS stands for data structure
        self.DS.update(attr)
        if ID==None:
            self.ID=-1 #To give each object a unique id. The value, -1, is replaced.
            self.ID_SET=False
        else:
            self.ID=ID #You should remember what you’re putting here, and it should be negative.
            self.ID_SET=True
    def clear(self):
        self.DS.clear()
    def __delitem__(self, key):
        del self.DS[key]
    def __getitem__(self, key):
        return self.DS[key]
    def __len__(self):
        return len(self.DS)
    def __iadd__(self, other):
        self.DS.update(other)
    def __isub__(self, other):
        for x in other:
            try:
                del self.DS[x]
            except KeyError:
                pass
    def __contains__(self, key):
        return self.DS.has_key(key)
    def __setitem__(self, key, value):
        self.DS[key]=value
    def __iter__(self):
        return iter(self.DS)
    def __eq__(self, other):
        return self.id==other.id
    def __ne__(self, other):
        return self.id!=other.id
    def __hash__(self):
        return self.id
    def __lt__(self, other):
        return self.id<other.id
    def __le__(self, other):
        return self.id<=other.id
    def __gt__(self, other):
        return self.id>other.id
    def __ge__(self, other):
        return self.id>=other.id
    @property
    def id(self):
        if self.ID_SET==False:
            print("Warning. self.id_set is False. You are accessing an id that has not been set.")
        return self.ID
    @id.setter
    def id(self, num):
        if self.ID_SET==True:
            raise ValueError("Once set, the id value may not be changed.")
        else:
            self.ID=num
            self.ID_SET=True
    def save(self, manager, commit=True):
        if self.ID_SET==False:
            self.id=manager.inc()
        manager.root.other_set.add(self)
        if commit==True:
            transaction.commit()

class Set(Persistent):
    def __init__(self, ID=None, *items):
        Persistent.__init__(self)
        self.DS=OOTreeSet()
        if ID==None:
            self.ID=-1 #To give each object a unique id. The value, -1, is replaced automatically when saved by the project for the first time (which should be done right after the object is created).
            self.ID_SET=False
        else:
            if ID>=0:
                raise ValueError("Manual values should be negative.")
            self.ID=ID #You should remember what you’re putting here, and it should be negative.
            self.ID_SET=True
        self.update(items)
    def update(self, items):
        self.DS.update(items)
    def add(self, *items):
        self.DS.update(items)
    def remove(self, *items):
        for x in items:
            self.DS.remove(x)
    def has(self, *items):
        for x in items:
            if not self.DS.has_key(x):
                return False
        return True
    def __len__(self):
        return len(self.DS)
    def __iadd__(self, other):
        self.DS.update(other)
    def __isub__(self, other):
        self.remove(*other)
    def __contains__(self, other):
        return self.DS.has_key(other)
    def __iter__(self):
        return iter(self.DS)
    def __eq__(self, other):
        return self.id==other.id
    def __ne__(self, other):
        return self.id!=other.id
    def __hash__(self):
        return self.id
    def __lt__(self, other):
        return self.id<other.id
    def __le__(self, other):
        return self.id<=other.id
    def __gt__(self, other):
        return self.id>other.id
    def __ge__(self, other):
        return self.id>=other.id
    @property
    def id(self):
        if self.ID_SET==False:
            print("Warning. self.id_set is False. You are accessing an id that has not been set.")
        return self.ID
    @id.setter
    def id(self, num):
        if self.ID_SET==True:
            raise ValueError("Once set, the id value may not be changed.")
        else:
            self.ID=num
            self.ID_SET=True
    def save(self, manager, commit=True):
        if self.ID_SET==False:
            self.id=manager.inc()
        manager.root.other_set.add(self)
        if commit==True:
            transaction.commit()

class Counter(Persistent):
    #This is for creating a persistent id count object (using a plain integer outside of a class doesn't seem to work).
    def __init__(self, value=0):
        self.value=value
        self.ID_SET=False
        self.id=value
    #The following methods are so it will fit fine in a BTree (they don't have anything to do with self.value)
    def __eq__(self, other):
        return self.id==other.id
    def __ne__(self, other):
        return self.id!=other.id
    def __hash__(self):
        return self.id
    def __lt__(self, other):
        return self.id<other.id
    def __le__(self, other):
        return self.id<=other.id
    def __gt__(self, other):
        return self.id>other.id
    def __ge__(self, other):
        return self.id>=other.id
    @property
    def id(self):
        if self.ID_SET==False:
            print("Warning. self.id_set is False. You are accessing an id that has not been set.")
        return self.ID
    @id.setter
    def id(self, num):
        if self.ID_SET==True:
            raise ValueError("Once set, the id value may not be changed.")
        else:
            self.ID=num
            self.ID_SET=True

class Manager:
    def __init__(self, filepath):
        self.filepath=filepath
        self.storage = ZODB.FileStorage.FileStorage(filepath)
        self.db = ZODB.DB(self.storage)
        self.conn = self.db.open()
        self.root = self.conn.root
        print("Database opened.\n")
        try:
            self.root.other_dict #This holds arbitrary stuff, like the Counter. String keys.
        except AttributeError:
            self.root.other_dict=OOBTree()
            self.root.other_dict["id_count"]=Counter()
        try:
            self.root.other_set #set other
        except AttributeError:
            self.root.other_set=OOTreeSet() #This holds all our Btree and Set objects (they are put here when saved to help them be persistent).
    def inc(self): #This increments our Counter and returns the new value to become the integer id of a new object.
        self.root.other_dict["id_count"].value+=1
        return self.root.other_dict["id_count"].value
    def close(self):
        self.db.pack()
        self.db.close()
        print("\nDatabase closed.")

class Btree2(Btree):
    #To prove that we can inherit our own classes we created that inherit Persistent (but inheriting OOBTree or OOTreeSet causes issues)
    def __init__(self, ID=None, **attr):
        Btree.__init__(self, ID, **attr)




m=Manager("/path/to/database/test.fs")

try:
    m.root.tree #Causes an AttributeError if this is the first time you ran the program, because it doesn't exist.
    print("OOBTree loaded.")
except AttributeError:
    print("Creating OOBTree.")
    m.root.tree=OOBTree()
    for i in range(5):
        key=Btree2()
        key.save(m, commit=False) #Saving without committing adds it to the manager's OOBTree and gives it an integer ID. This needs to be done right after creating an object (whether or not you commit).
        value=Btree2()
        value.save(m, commit=False)
        m.root.tree[key]=value #Assigning key and value (which are both objects) to the OOBTree
    transaction.commit() #Commit the transactions

try:
    m.root.set
    print("OOTreeSet loaded.")
except AttributeError:
    print("Creating OOTreeSet")
    m.root.set=OOTreeSet()
    for i in range(5):
        item=Set()
        item.save(m, commit=False)
        m.root.set.add(item)
    transaction.commit()

#Doing the same with an OOTreeSet (since objects in them suffered from the same problem as objects as keys in an OOBTree)
for x in m.root.tree:
    print("Key: "+str(x.id))
    print("Value: "+str(m.root.tree[x].id))
    if x in m.root.tree:
        print("Comparison works for "+str(x.id))

print("\nOn to OOTreeSet.\n")

for x in m.root.set:
    if x in m.root.set:
        print("Comparison works for "+str(x.id))

m.close()