2012-02-16 24 views
10

Mam kosztowną funkcję, która pobiera i zwraca niewielką ilość danych (kilka liczb całkowitych i liczb zmiennoprzecinkowych). Mam już tę funkcję, ale chciałbym, aby ta notatka była trwała. Istnieje już kilka wątków odnoszących się do tego, ale jestem pewien na temat potencjalnych problemów z niektórymi z proponowanych rozwiązań, a mam pewne dość specyficzne wymagania:Trwałe zapamiętywanie w Pythonie

  • I na pewno użyć funkcji z wielu wątków i procesy jednocześnie (zarówno przy użyciu multiprocessing iz oddzielnych skryptów Pythona)
  • i nie trzeba czytać lub pisać dostęp do notatki spoza tej funkcji Pythona
  • ja nie jestem tym zaniepokojony notatki są uszkodzone w rzadkich przypadkach (jak wyrywanie wtyczkę lub przypadkowo zapisując w pliku bez blokowania), ponieważ nie jest to drogi do odbudowy (zwykle 10-20 minut), ale wolałbym, żeby nie został uszkodzony z powodu wyjątków lub ręcznie przerwał proces python (nie wiem, jak realistyczny to jest)
  • Zdecydowanie wolałbym rozwiązania które nie wymagają dużych bibliotek zewnętrznych, ponieważ mam bardzo ograniczoną ilość miejsca na dysku twardym na jednej maszynie będę uruchamiać kod na
  • Mam słabe preferencje dla kodu między platformami, ale prawdopodobnie użyję tego tylko w systemie Linux:

This thread omawia moduł shelve, który najwyraźniej nie jest bezpieczny dla procesu. Dwie z odpowiedzi sugerują użycie fcntl.flock do zablokowania pliku półki. Niektóre z odpowiedzi w this thread wydają się sugerować, że jest to brzemienny problem - ale nie jestem do końca pewien, co to jest. Wydaje się, że jest to ograniczone do Uniksa (chociaż podobno Windows ma odpowiednik o nazwie msvcrt.locking), a blokada jest tylko "doradcza" - to znaczy, nie powstrzyma mnie przed przypadkowym zapisaniem do pliku bez sprawdzenia, czy jest zablokowana. Czy są jakieś inne potencjalne problemy? Czy zapisanie kopii pliku i zastąpienie kopii wzorcowej końcowym krokiem zmniejszy ryzyko korupcji?

Nie wygląda na to, że dbm module będzie lepiej niż półka. Szybko rzuciłem okiem na sqlite3, ale wydaje mi się to nieco przesadzone. This thread i this one wspominają o bibliotekach stron trzecich, w tym o ZODB, ale istnieje wiele możliwości wyboru i wszystkie wydają się zbyt duże i skomplikowane w tym zadaniu.

Czy ktoś ma jakieś porady?

UPDATE: uprzejmie wspomniałem IncPy poniżej, co wygląda bardzo interesująco. Niestety nie chciałbym wrócić do Pythona 2.6 (w rzeczywistości używam wersji 3.2) i wygląda na to, że korzystanie z bibliotek C jest nieco niewygodne (między innymi używam potężnego numpy i scipy).

inny pomysł kindall jest pouczający, ale myślę, że dostosowanie go do wielu procesów byłoby trochę trudne - przypuszczam, że najłatwiej byłoby zastąpić kolejkę blokowaniem pliku lub bazą danych.

Patrząc ponownie na ZODB, wygląda idealnie na to zadanie, ale naprawdę chcę uniknąć używania dodatkowych bibliotek. Nadal nie jestem do końca pewien, jakie są wszystkie problemy związane z używaniem tylko flock - wyobrażam sobie, że jednym dużym problemem jest to, czy proces jest kończony podczas zapisywania do pliku, czy przed zwolnieniem blokady?

Więc wziąłem porady synthesizerpatel i poszedł z sqlite3. Jeśli ktoś jest zainteresowany, postanowiłem zrobić zamiennik dla dict który przechowuje swoje dane jak ogórki w bazie danych (nie przeszkadza, aby zachować w pamięci jako dowolny dostęp do bazy danych i marynowanie jest wystarczająco szybki w porównaniu do wszystkiego innego jestem robić). Jestem pewien, że są bardziej skuteczne sposoby osiągnięcia tego celu (i nie mam pojęcia, czy może nadal mam problemy współbieżności), ale tutaj jest kod:

from collections import MutableMapping 
import sqlite3 
import pickle 


class PersistentDict(MutableMapping): 
    def __init__(self, dbpath, iterable=None, **kwargs): 
     self.dbpath = dbpath 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'create table if not exists memo ' 
       '(key blob primary key not null, value blob not null)' 
      ) 
     if iterable is not None: 
      self.update(iterable) 
     self.update(kwargs) 

    def encode(self, obj): 
     return pickle.dumps(obj) 

    def decode(self, blob): 
     return pickle.loads(blob) 

    def get_connection(self): 
     return sqlite3.connect(self.dbpath) 

    def __getitem__(self, key): 
     key = self.encode(key) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select value from memo where key=?', 
       (key,) 
      ) 
      value = cursor.fetchone() 
     if value is None: 
      raise KeyError(key) 
     return self.decode(value[0]) 

    def __setitem__(self, key, value): 
     key = self.encode(key) 
     value = self.encode(value) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'insert or replace into memo values (?, ?)', 
       (key, value) 
      ) 

    def __delitem__(self, key): 
     key = self.encode(key) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select count(*) from memo where key=?', 
       (key,) 
      ) 
      if cursor.fetchone()[0] == 0: 
       raise KeyError(key) 
      cursor.execute(
       'delete from memo where key=?', 
       (key,) 
      ) 

    def __iter__(self): 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select key from memo' 
      ) 
      records = cursor.fetchall() 
     for r in records: 
      yield self.decode(r[0]) 

    def __len__(self): 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select count(*) from memo' 
      ) 
      return cursor.fetchone()[0] 
+2

Jeśli można radzić sobie z Python 2.6.3 i nie są w systemie Windows, może chcesz sprawdzić [IncPy] (http://www.stanford.edu/~pgbovine/incpy.html), który będzie automatycznie uporczywie zapamiętuj swój * cały program * wszędzie, gdzie jest to bezpieczne. – kindall

Odpowiedz

7

sqlite3 z pudełka zapewnia ACID. Blokowanie plików jest podatne na problemy z wyścigami i problemy z współbieżnością, których nie będziesz musiał używać w sqlite3.

Zasadniczo tak, sqlite3 jest więcej niż to, co trzeba, ale nie jest to ogromne obciążenie. Może działać na telefonach komórkowych, więc nie jest tak, że chcesz uruchomić jakieś bestialskie oprogramowanie. Pozwoli to zaoszczędzić czas na przerabianiu kół i debugowaniu problemów z blokowaniem.

6

Zakładam, że chcesz, aby kontynuować memoize wyniki funkcji w pamięci RAM, prawdopodobnie w słowniku, ale użyć wytrwałości, aby zmniejszyć „nagrzewania” czas aplikacji. W tym przypadku nie będziesz losowo uzyskiwania dostępu do pozycji bezpośrednio w sklepie kopii zapasowej, więc baza danych może być rzeczywiście przesadą (choć jako synthesizerpatel zauważa, może nie tak dużo, jak myślisz).

Mimo to, jeśli chcesz toczyć własną, realną strategią może być po prostu załadować z pliku słownika na początku biegu przed rozpoczęciem jakichkolwiek wątków. Jeśli wynik nie znajduje się w słowniku, musisz go zapisać w pliku po dodaniu go do słownika. Możesz to zrobić, dodając go do kolejki i używając pojedynczego wątku roboczego, który opróżnia elementy z kolejki na dysk (wystarczy dołączenie ich do pojedynczego pliku byłoby w porządku). Czasami możesz dodać ten sam wynik więcej niż jeden raz, ale nie jest to krytyczne, ponieważ za każdym razem będzie to ten sam wynik, więc przeczytanie go dwa razy lub więcej nie przyniesie prawdziwej szkody. Python's threading model pozwoli uniknąć większości problemów związanych z współbieżnością (np. Dołączanie do listy ma charakter atomowy).

Oto (niesprawdzone, ogólny, niekompletny) kod pokazując to, co mówię:

import cPickle as pickle 

import time, os.path 

cache = {} 
queue = [] 

# run at script start to warm up cache 
def preload_cache(filename): 
    if os.path.isfile(filename): 
     with open(filename, "rb") as f: 
      while True: 
       try: 
        key, value = pickle.load(f), pickle.load(f) 
       except EOFError: 
        break 
       cache[key] = value 

# your memoized function 
def time_consuming_function(a, b, c, d): 
    key = (a, b, c, d) 
    if key in cache: 
     return cache[key] 
    else: 
     # generate the result here 
     # ... 
     # add to cache, checking to see if it's already there again to avoid writing 
     # it twice (in case another thread also added it) (this is not fatal, though) 
     if key not in cache: 
      cache[key] = result 
      queue.append((key, result)) 
     return result 

# run on worker thread to write new items out 
def write_cache(filename): 
    with open(filename, "ab") as f: 
     while True: 
      while queue: 
       key, value = queue.pop() # item order not important 
       # but must write key and value in single call to ensure 
       # both get written (otherwise, interrupting script might 
       # leave only one written, corrupting the file) 
       f.write(pickle.dumps(key, pickle.HIGHEST_PROTOCOL) + 
         pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) 
      f.flush() 
      time.sleep(1) 

Gdybym miał czas, chciałbym przekształcić dekorator ... i umieścić wytrwałości w podklasę dict ... wykorzystanie zmiennych globalnych jest również nieoptymalne. :-) Jeśli użyjesz tego podejścia z multiprocessing, prawdopodobnie będziesz używał raczej multiprocessing.Queue niż listy; następnie można użyć queue.get() jako blokowania oczekiwania na nowy wynik w procesie roboczym, który zapisuje do pliku. Nie używałem jednak multiprocessing, więc weź tę odrobinę rady z przymrużeniem oka.