如何实现一个持久化的Python列表?

8

我想让一个对象像内置的list一样运行,但其值在修改后可以保存。

我想到的实现方法是将list包装在PersistentList类中。对于每次可能更改列表的方法的访问,包装器会委托给包装的list,并在调用后将其保存到键值数据库中。

代码:

class PersistentList(object):
    def __init__(self, key):
        self.key = key 
        self._list = db.get(key, []) 

    def __getattr__(self, name):
        attr = getattr(self._list, name)
        if attr:
            if attr in ('append', 'extend', 'insert', 'pop',
                'remove', 'reverse', 'sort'):
                attr = self._autosave(attr)
            return attr
        raise AttributeError

    def _autosave(self, func):
        @wraps(func)
        def _(*args, **kwargs):
            ret = func(*args, **kwargs)
            self._save()
            return ret 
        return _

    def _save(self):
        db.set(self.key, self._list)

这个实现存在几个问题:

  1. 每次访问像 append 这样的方法时,都需要装饰一次,有没有更好的方法来装饰某个对象的多个方法?

  2. 由于我没有实现 iadd 方法,因此像 l += [1,2,3] 这样的操作不起作用。

我该怎么简化这个实现?


如果你调用的列表方法之一引发了异常怎么办?你仍然想要保存吗?你当前的解决方案仍然可以实现... - Ioan Alexandru Cucu
4个回答

6

我喜欢@andrew cooke的答案,但我认为你可以直接从列表中派生。

class PersistentList(list):
    def __init__(self, *args, **kwargs):
        for attr in ('append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'):
            setattr(self, attr, self._autosave(getattr(self, attr))
        list.__init__(self, *args, **kwargs)
    def _autosave(self, func):
        @wraps(func)
        def _func(*args, **kwargs):
            ret = func(*args, **kwargs)
            self._save()
            return ret 
        return _func

你的 _save 是什么样子的?你如何将对象加载回来?我使用 pickle 的天真尝试失败了。pickle.dumps(self) 不起作用,而 pickle.dumps(list(self)) 则可以。或者你会在每次 _save 运行时都将其转换为列表吗? - kuzzooroo
另外,是什么让你有信心在你的变异器列表中不包括 '__delitem__', '__delslice__', '__iadd__', '__imul__', '__reversed__', '__setitem__', '__setslice__' - kuzzooroo
不知道_save是什么样子。我相信我是基于一个已被删除的答案来编写的。您应该包括任何可能修改列表数据的函数,包括您提到的大多数函数(尽管我不认为__reversed__是一个mutator)。 - mattbornski
关于__reversed__的有趣点。我通过执行set(dir(list)) - set(dir(tuple))得到了那些方法列表。我猜创建迭代器需要以某种方式改变列表。另一方面,reversed(没有下划线)适用于元组。我创建了一个新问题来找出原因。 - kuzzooroo

3
这里有一种方法可以避免必须装饰每个列表方法。它使得PersistentList成为一个“上下文管理器”,因此您可以使用:上下文管理器
with PersistentList('key', db) as persistent:
    do_stuff()

语法。诚然,这并不会在每个列表操作后调用_save方法,只有在退出with-block时才会调用。但我认为它给了你足够的控制权,在你想要保存的时候进行保存,特别是因为__exit__方法保证会被执行,无论你如何离开with-block,包括异常情况。

你可能会发现_save不会在每次列表操作后都被调用是一个优势。想象一下向列表中添加10,000次。那么许多对db.set(一个数据库?)的单独调用可能会非常耗时。从性能角度来看,最好是将所有的追加和保存操作一次完成。


class PersistentList(list):
    def __init__(self, key, db):
        self.key = key
        self.extend(db.get(key, []))
    def _save(self):
        # db.set(self.key, self)
        print('saving {x}'.format(x = self))
    def __enter__(self):
        return self
    def __exit__(self,ext_type,exc_value,traceback):
        self._save()

db = {}
p = PersistentList('key', db)

with p:
    p.append(1)
    p.append(2)

with p:
    p.pop()
    p += [1,2,3]

# saving [1, 2]
# saving [1, 1, 2, 3]

如果你想的话,你甚至可以通过混合这两种技术来变得更加高级,以便保持一个“脏”标志,指示需要保存。你甚至可以让 PersistenList.__del__ 抱怨或尝试保存(如果系统正在退出,它可能会失败)如果它是脏的。 - Chris Morgan
@ChrisMorgan:我喜欢你的想法,但我认为正确实现可能会很困难。例如,如果用户先执行append再执行pop,那么一个天真的实现(通过装饰每个列表方法)会错误地设置dirty标志。要做得更好,您需要在__enter__中保存列表的副本,并在每个列表方法中测试列表是否已更改。所有这些比较可能会使性能变慢。由于通常希望保存,因此最好有点浪费,每次都保存。 - unutbu
我只是把它作为一个基本的指示放进去,表明事情已经发生了改变。当然,这些更改可能已经被撤消,但正如你所说,防止不必要的写入带来的成本太高了。 - Chris Morgan
@ChrisMorgan:啊,我明白了。在这种情况下,如果用户使用 with p,那么我们可以假设 p 将会被更改。因此,我们可以认为脏标记将被设置。我们可以在 __enter__ 中保存原始列表的副本,并在 __exit__ 中进行一次比较,以查看是否需要调用 _save。这将避免装饰所有列表方法的情况。 - unutbu

0

我知道这不够美观或聪明,但我只会把每个方法都写出来...

class PersistentList(object):
   ...

   def append(self, o):
      self._autosave()
      self._list.append(o)

   ...etc...

0
这里有一个与@unutbu非常相似但更通用的答案。它提供了一个函数,您可以调用该函数将对象同步到磁盘,并且它适用于除list之外的其他可pickle类。
with pickle_wrap(os.path.expanduser("~/Desktop/simple_list"), list) as (lst, lst_sync):
    lst.append("spam")
    lst_sync()
    lst.append("ham")
    print(str(lst))
    # lst is synced one last time by __exit__

这是使其成为可能的代码:
import contextlib, pickle, os, warnings

def touch_new(filepath):
    "Will fail if file already exists, or if relevant directories don't already exist"
    # https://dev59.com/InM_5IYBdhLWcg3wgzdl#1348073
    os.close(os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_EXCL))

@contextlib.contextmanager
def pickle_wrap(filepath, make_new, check_type=True):
    "Context manager that loads a file using pickle and then dumps it back out in __exit__"
    try:
        with open(filepath, "rb") as ifile:
            result = pickle.load(ifile)
        if check_type:
            new_instance = make_new()
            if new_instance.__class__ != result.__class__:
                # We don't even allow one class to be a subclass of the other
                raise TypeError(("Class {} of loaded file does not match class {} of "
                    + "value returned by make_new()")
                    .format(result.__class__, new_instance.__class__))
    except IOError:
        touch_new(filepath)
        result = make_new()
    try:
        hash(result)
    except TypeError:
        pass
    else:
        warnings.warn("You probably don't want to use pickle_wrap on a hashable (and therefore likely immutable) type")

    def sync():
        print("pickle_wrap syncing")
        with open(filepath, "wb") as ofile:
            pickle.dump(result, ofile)

    yield result, sync
    sync()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接