2012-02-16 2 views
10

У меня есть дорогостоящая функция, которая берет и возвращает небольшое количество данных (несколько целых чисел и поплавков). У меня уже есть memoized эта функция, но я хотел бы сделать напоминание постоянным. Есть уже несколько нитей, связанные с этим, но я не уверен о возможных проблемах с некоторыми из предложенных подходов, и у меня есть некоторые довольно специфические требования:Постоянная memoization в Python

  • Я определенно использовать функцию из нескольких потоков и процессы одновременно (как с использованием multiprocessing и из отдельных сценариев Python)
  • мне не нужно читать и писать доступ к памятке из вне этой питона функции
  • Я не то, что озабоченности по поводу записки коррумпируются в редких случаях (например, потянув вилку или случайно записать в файл без ее блокировки), поскольку это не , что дорогостоящий для восстановления (обычно 10-20 минут), но я бы предпочел, чтобы он не был поврежден из-за исключений или вручную завершил процесс python (я не знаю, насколько это реально)
  • Я бы предпочел решения которые не требуют больших внешних библиотек, поскольку у меня на жестком диске имеется ограниченное количество места на жестком диске. Я буду запускать код на
  • У меня слабое предпочтение для кросс-платформенного кода, но я, скорее всего, буду использовать этот на Linux

This thread обсуждает модуль shelve, который, по-видимому, не является безопасным процессом. Два из них предлагают использовать fcntl.flock для блокировки файла полки. Однако некоторые из ответов в this thread, похоже, предполагают, что это чревато проблемами - но я не совсем уверен, что это такое. Похоже, что это ограничивается Unix (хотя, видимо, Windows имеет эквивалент, называемый msvcrt.locking), а блокировка - только «консультативная», то есть это не помешает мне случайно записать в файл, не проверяя, что он заблокирован. Существуют ли другие потенциальные проблемы? Написал бы копию файла и заменил бы главную копию как последний шаг, уменьшив риск коррупции?

Не похоже, что dbm module будет лучше, чем полка. Я быстро посмотрел на sqlite3, но для этой цели это кажется немного излишним. This thread и this one упоминают несколько сторонних библиотек, в том числе ZODB, но есть много вариантов, и все они кажутся слишком большими и сложными для этой задачи.

У кого-нибудь есть совет?

ОБНОВЛЕНИЕ: kindall упоминается IncPy ниже, что выглядит очень интересно. К сожалению, я бы не хотел возвращаться к Python 2.6 (я на самом деле использую 3.2), и похоже, что это немного неудобно использовать с библиотеками C (среди прочих, я использую numpy и scipy).

kindall другая идея поучительна, но я думаю, что адаптация этого к нескольким процессам была бы немного сложной - я полагаю, было бы проще заменить очередь на блокировку файла или базу данных.

Глядя на ZODB снова, он отлично подходит для задачи, но я действительно хочу избежать использования каких-либо дополнительных библиотек. Я все еще не совсем уверен, что все проблемы с просто использованием flock - я думаю, что одна большая проблема заключается в том, что процесс завершается во время записи в файл или до освобождения блокировки?

Итак, я взял совет synhesizerpatel и ушел с sqlite3. Если кому-то это интересно, я решил сделать замену на dict, которая хранит свои записи в виде соленья в базе данных (я не хочу хранить в памяти, поскольку доступ к базе данных и травление достаточно быстро по сравнению со всем остальным, что я делать). Я уверен, что есть более эффективные способы сделать это (и я понятия не имею, могу ли я все еще есть проблемы параллелизма), но вот код:

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

Если вы имеете дело с Python 2.6.3 и не находитесь в Windows, вы можете проверить [IncPy] (http://www.stanford.edu/~pgbovine/incpy.html), который будет автоматически и постоянно сохраняйте свою * всю программу * везде, где это безопасно. – kindall

ответ

7

sqlite3 из коробки обеспечивает ACID. Блокировка файлов подвержена условиям гонки и проблемам параллелизма, которые у вас не будет использовать sqlite3.

В принципе, да, sqlite3 больше, чем вам нужно, но это не огромная нагрузка. Он может работать на мобильных телефонах, поэтому не похоже, что вы совершаете запуск какого-нибудь зверского программного обеспечения. Это избавит вас от необходимости изобретать колеса и отлаживать проблемы с блокировкой.

6

Я предполагаю, что вы хотите продолжать запоминать результаты функции в ОЗУ, возможно, в словаре, но используйте упорство, чтобы уменьшить время прогрева приложения. В этом случае вы не будете случайным образом получать доступ к элементам непосредственно в резервном магазине, поэтому база данных действительно может быть переполнена (хотя, как видно, synhesizerpatel отмечает, может быть, не так много, как вы думаете).

Тем не менее, если вы хотите сворачивать самостоятельно, жизнеспособной стратегией может быть просто загрузка словаря из файла в начале вашего запуска перед началом любых потоков. Когда результат не находится в словаре, вам необходимо записать его в файл после добавления его в словарь. Вы можете сделать это, добавив его в очередь и используя один рабочий поток, который сбрасывает элементы из очереди на диск (просто добавление их в один файл будет в порядке). Иногда вы можете добавлять один и тот же результат не один раз, но это не фатально, так как каждый раз он будет одинаковым результатом, поэтому повторное чтение его дважды или больше не наносит реального вреда. Потоковая модель Python избавит вас от большинства проблем с параллелизмом (например, добавление в список является атомарным).

Вот некоторые (не проверялось, общий, неполный) код, показывающий, что я говорю о:

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) 

Если у меня было время, я бы превратить это в декоратор ... и поставить настойчивость в подкласс dict ... использование глобальных переменных также неоптимально. :-) Если вы используете этот подход с multiprocessing, вы, вероятно, захотите использовать multiprocessing.Queue, а не список; вы можете использовать queue.get() в качестве блокирующего ожидания для нового результата в рабочем процессе, который записывает в файл. Однако я не использовал multiprocessing, поэтому возьмите этот кусочек совета с солью.