@theurus

Есть ли какие то варианты вызвать деструктор принудительно кроме обязательного использования контекстного менеджера?

Пытаюсь сделать объект - копию обычного словаря но с дополнительной функцией авто-сохранения на диск.

Сохранять можно при записи, например в обработчике __setitem__ но у этого варианта есть проблема с тем что в этот обработчик попадает только верхний уровень
d[1] = 2 # сработает
d[1][2] = 3 # не сработает, тут сработает обработчик объекта d[1].__setitem__ который не имеет ничего общего с d.__setitem__ а d просто не заметит что у него внутри что то изменилось

Что бы замечать изменения глубже 1 уровня попробовал использовать постоянно работающий поток который делает хеш содержимого словаря и сверяет с предыдущим, так всё работает но есть проблема с завершением программы.

Программа не завершается если вручную не остановить этот поток. Вручную вызвать .stop() или использовать with. Я хочу что бы поток принудительно останавливался когда объект выходит из зоны видимости, завершается программа или функция в которой он создан.

#!/usr/bin/env python3


import pickle
import time
import threading
import sqlitedict


class PersistDict:
    def __init__(self, filename = 'test.pickle', timer = 2):
        self.filename = filename
        self.data = {}
        self.last_hash = None
        self.timer = timer
        self.lock = threading.Lock()

        self.load()

        self._thread = threading.Thread(target=self.run)
        self.running = True
        self._thread.start()

    def __del__(self):
        """да я вкурсе что это не деструктор в том смысле что у c++
        вопрос тут как с этим быть, если хочется что бы объект удалялся
        (останавливался поток) при выходе из области видимости?
        без контекстного менеджера и прямого вызова .stop()
        """
        self.stop()

    def stop(self):
        self.running = False
        self._thread.join()
        self.check_for_changes()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()

    def __getitem__(self, key):
        return self.data[key]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __delitem__(self, key):
        del self.data[key]

    def __contains__(self, key):
        return key in self.data

    def __reversed__(self):
        return reversed(self.data)

    def __eq__(self, other):
        return self.data == other
    
    def __hash__(self):
        return hash(pickle.dumps(self.data))

    def __ne__(self, other):
        return self.data != other

    def __len__(self):
        return len(self.data)

    def __iter__(self):
        return iter(self.data)

    def clear(self):
        self.data.clear()

    # def copy(self):
    #     return PersistDict(self.filename, self.data.copy())

    @classmethod
    def fromkeys(cls, keys, value=None):
        return cls({key: value for key in keys})

    def get(self, key, default=None):
        return self.data.get(key, default)

    def items(self):
        return self.data.items()

    def keys(self):
        return self.data.keys()

    def pop(self, key, default=None):
        value = self.data.pop(key, default)
        return value

    def popitem(self):
        key, value = self.data.popitem()
        return key, value

    def setdefault(self, key, default=None):
        value = self.data.setdefault(key, default)
        return value

    def update(self, other):
        self.data.update(other)

    def values(self):
        return self.data.values()

    def load(self):
        try:
            with open(self.filename, 'rb') as f:
                self.data = pickle.load(f)
            self.last_hash = hash(pickle.dumps(self.data))
        except FileNotFoundError:
            pass

    def save(self):
        with self.lock:
            with open(self.filename, 'wb') as f:
                print('saving')
                pickle.dump(self.data, f)

    def check_for_changes(self):
        new_hash = hash(pickle.dumps(self.data))
        if new_hash != self.last_hash:
            self.save()
            self.last_hash = new_hash

    def run(self):
        while self.running:
            time.sleep(self.timer)
            self.check_for_changes()
            print('running')


if __name__ == '__main__':
    # этот вариант работает как задумано но требут контекстного менеджера
    # with PersistDict() as d:
    #     for x in range(10):
    #         d[x] = x+1
    #         time.sleep(1)
    #         print(d[x])


    # этот вариант работает как задумано но требует ручного вызова функции stop
    # d = PersistDict()
    # for x in range(10):
    #     d[x] = x+1
    #     time.sleep(1)
    #     print(d[x])
    # d.stop()


    # в этом варианте программа не завершается
    # объект не уничтожается, поток не останавливается,
    # хотя vscode показывает что потоков больше нет
    # d = PersistDict()
    # for x in range(10):
    #     d[x] = x+1
    #     time.sleep(1)
    #     print(d[x])


    # тот же результат что и в предыдущем варианте
    # попытка удалить объект вручную приводит к.. ничему. это вообще не работает,
    # функция __del__ не вызывается(ставил туда точку останова и принты),
    # но и ошибок никаких не порождает, до финиша доходит в любом случае
    d = PersistDict()
    for x in range(10):
        d[x] = x+1
        time.sleep(1)
        print(d[x])
    del d


    print('finished')
  • Вопрос задан
  • 98 просмотров
Пригласить эксперта
Ответы на вопрос 2
fenrir1121
@fenrir1121
Начни с документации
В стандартной библиотеке уже реализован shelve, который делает ровно то же самое причем тоже с помощью pickle.

Но pickle медленный и не безопасный, я бы советовал просто настроить redis, потому что есть Redis persistence
Ответ написан
Vindicar
@Vindicar
RTFM!
Ну во-первых, ты вот так сходу это не реализуешь, потому что помимо вложенных словарей есть списки и другие коллекции, а также есть классы. Ты, конечно, можешь схитрить - сделать класс-прокси, который сигнализирует об изменениях при __setattr__() и __setitem()__, а также реализует вызовы __getattr__() и __getitem__() так, чтобы возвращать такой же прокси для целевого объекта.
Условно:
l = [1]
d = {'a': l}
pd = MyProxy(d)
lst = pd['a']  # на самом деле lst это MyProxy(l, owner=pd)
lst[0] = 2  # прокси-список реагирует на присваивание и оповещает владлеьца - прокси-словарь

Но и это не очень хорошо, так как в примере выше я могу изменить список через ссылку l. Не говоря уже о проблемах с проверками типов данных. Так что сложно-составные структуры персистентными сделать малореально.

Я бы сказал, вообще не очень хорошая идея реализовывать персистентность вот так.
Вариант А: делай снапшоты в фиксированные моменты времени. Если сохранение снапшота слишком долгое, сделай клон через deepcopy и сохраняй клон, пока оригинал изменяется дальше.
Вариант Б: явно сохраняй изменения, которые должны быть персистентными. Каждое в отдельности.
Вариант В: Используй память, отображаемую на диск. Тогда ОС сама будет периодически сбрасывать изменившиеся страницы памяти на диск (хотя есть метод flush()).
Но в этом случае может потребоваться изменить структуру данных, избавившись по максимуму от сложных объектов типа словарей и заменив их на простые массивы байт. Если нужны структуры, то их придётся распаковывать с помощью struct. Т.е. опять приходим к тому, что структура не должна быть сложной.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы