Постоянная memoization в Python

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

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

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

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

Есть ли у кого-нибудь советы?

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

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

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

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

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]

Ответ 1

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

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

Ответ 2

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

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

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

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 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, хотя, так что возьмите этот кусочек совета с солью.