Постоянное запоминание в Python - PullRequest
10 голосов
/ 17 февраля 2012

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

  • Я обязательно буду использовать функцию из нескольких потоков и процессов одновременно (как с использованием 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).

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

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

Итак, я воспользовался советом synthesizerpatel и ушел с 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 ]

7 голосов
/ 17 февраля 2012

sqlite3 из коробки обеспечивает КИСЛОТА . Блокировка файлов подвержена гоночным условиям и проблемам параллелизма, которых у вас не будет при использовании sqlite3.

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

6 голосов
/ 17 февраля 2012

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

Тем не менее, если вы хотите накатить свою собственную, жизнеспособной стратегией может быть простая загрузка словаря из файла в начале выполнения перед запуском любых потоков.Если результат отсутствует в словаре, вам необходимо записать его в файл после добавления в словарь.Вы можете сделать это, добавив его в очередь и используя один рабочий поток, который сбрасывает элементы из очереди на диск (просто добавьте их в один файл).Иногда вы можете добавлять один и тот же результат более одного раза, но это не фатально, поскольку каждый раз будет один и тот же результат, поэтому повторное чтение его дважды или более не принесет реального вреда.Модель потоков 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, поэтому примите этот совет с крошкой соли.

...