2012-02-26 9 views
6

Próbuję sprawić, aby obiekt działał jak wbudowany list, z tą różnicą, że jego wartość zostanie zapisana po modyfikacji.Jak zaimplementować trwałą listę `Pythona '?

Implementacja, którą wymyślam, jest opakowaniem klasy list w klasie PersistentList. W przypadku każdego dostępu do metody, która może zmienić listę, opakowanie jest przekazywane do zapakowanego pliku list i zapisywane w bazie danych klucz-wartość po jej wywołaniu.

Kod:

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) 

Istnieje kilka problemów z tym realizacji:

  1. mam do dekorowania metod jak append każdym razem są one obejrzano, czy istnieje lepszy sposób, aby ozdobić wielokrotność metody jakiegoś obiektu?

  2. operacje jak l += [1,2,3] nie działa, ponieważ nie mam wdrożył metodę iadd.

Co mogę zrobić, aby to uprościć?

+0

Co jeśli jedna z metod lista nawiązywać połączenia podnosi wyjątek? Nadal chcesz zrobić save? Obecne rozwiązanie nadal to robi ... –

Odpowiedz

4

Lubię odpowiedź @andrew Cooke, ale nie widzę powodu, dlaczego nie można wyprowadzić bezpośrednio z lista.

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 
+0

Tak, to jest lepsze. –

+0

Jak wygląda twoje '_save'? A jak załadować obiekt z powrotem? Moje naiwne próby zrobienia tego za pomocą 'marynarki 'nie działają. 'pickle.dumps (self)' nie działa podczas 'pickle.dumps (list (self))'. A może po prostu konwertowałbyś do listy za każdym razem, gdy '_save' działa()? – kuzzooroo

+0

Ponadto, co utwierdziło Cię w przekonaniu, że nie musiałeś podawać "'__delitem__', '__delslice__', '__iadd__', '__imul__', '__reversed__', '__setitem__', '__setslice __' 'na swojej liście mutatorów? – kuzzooroo

0

wiem, że to nie jest całkiem albo mądry, ale ja po prostu napisać poszczególne metody z ...

class PersistentList(object): 
    ... 

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

    ...etc... 
3

Oto sposób na uniknięcie konieczności ozdabiania każdej metody listy. To sprawia, że ​​PersistentList context manager, dzięki czemu można używać składni z

with PersistentList('key', db) as persistent: 
    do_stuff() 

. Wprawdzie nie powoduje to wywołania metody po każdej operacji na liście, tylko po wyjściu z with-block. Ale myślę, że daje ci wystarczającą kontrolę, aby zaoszczędzić, gdy chcesz zaoszczędzić, zwłaszcza, że ​​metoda __exit__ jest gwarantowana do wykonania bez względu na to, jak opuścisz with-block, w tym, jeśli dzieje się to z powodu wyjątku.

Możesz być zaletą, że _save nie jest wywoływana po każdej operacji na liście. Wyobraź sobie, że dołączasz do listy 10 000 razy. Tak wiele pojedynczych połączeń z db.set (baza danych?) Może być dość czasochłonne. Byłbym lepszy, przynajmniej z punktu widzenia wydajności, aby wszystkie dodatki i oszczędzać raz.


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] 
+0

Jeśli chcesz, możesz się jeszcze bardziej urozmaicić, łącząc dwie techniki, aby zachować "brudną" flagę wskazującą, że należy ją zapisać. Możesz nawet uczynić tak, że 'PersistenList .__ del__' będzie narzekać lub próbować zapisać (jeśli system wyjdzie, może się nie powieść), jeśli jest brudny. –

+0

@ChrisMorgan: Podoba mi się twój pomysł, ale myślę, że byłoby to trudne do prawidłowego wdrożenia. Na przykład, jeśli użytkownik miałby "dołączyć", a następnie "pop", naiwna implementacja (przez dekorowanie każdej metody listy) spowodowałaby błędne ustawienie flagi 'brudnej'. Aby to zrobić lepiej, musisz zapisać kopię listy w "__enter__" iw każdym testowaniu listy list, jeśli lista jest brudna. Wszystkie te porównania mogą spowalniać wykonanie. Ponieważ generalnie chciałbyś oszczędzać, może lepiej być trochę marnotrawnym i po prostu oszczędzać za każdym razem. – unutbu

+0

Położyłem to tylko jako podstawowy wskaźnik, że rzeczy zostały zmienione. Oczywiście, zmiany mogły zostać cofnięte, ale, jak mówisz, koszt zapobiegania niepotrzebnemu zapisowi byłby zbyt wysoki. –

0

Oto odpowiedź, że to dużo jak @ unutbu, ale bardziej ogólnie. Daje ci funkcję, którą możesz wywołać, aby zsynchronizować twój obiekt z dyskiem i współpracuje z innymi klasami, oprócz list.

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__ 

Oto kod, który sprawia, że ​​to możliwe:

import contextlib, pickle, os, warnings 

def touch_new(filepath): 
    "Will fail if file already exists, or if relevant directories don't already exist" 
    # http://stackoverflow.com/a/1348073/2829764 
    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() 
Powiązane problemy